Как создать сущность в Drupal

В сегодняшнем уроке я расскажу, как написать свою сущность. Писать мы будем с применением модуля Entity API, поэтому нам необходимо скачать и установить этот модуль. Для примера, создадим сущность product. Файловая структура модуля у меня получилась такая:

  • product
    • product.info
    • product.install
    • product.module
    • product.pages.inc
    • product.admin.inc
    • js
      • product-fieldset-summaries.js
    • templates
      • product.tpl.php

1. Создаем информацию об модуле в product.info:

name = Product
description = Defines the product entity and associated features.
core = 7.x
dependencies[] = entity

2. В product.install описываем таблицу, в которой будет хранится информация об сущностях:

/**
 * Implements hook_schema().
 */
function product_schema() {
  $schema['product'] = array(
    'description' => 'The base table for products.',
    'fields' => array(
      'id' => array(
        'description' => 'The primary identifier for a product.',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'title' => array(
        'description' => 'The title of this product, always treated as non-markup plain text.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => TRUE,
        'default' => '',
      ),
      'uid' => array(
        'description' => 'The {users}.uid that owns this product.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
      'status' => array(
        'description' => 'Boolean indicating whether the status of this product',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 1,
      ),
      'created' => array(
        'description' => 'The Unix timestamp when the product was created.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
      'changed' => array(
        'description' => 'The Unix timestamp when the product was most recently saved.',
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
    ),
    'primary key' => array('id'),
    'indexes' => array(
      'uid' => array('uid'),
      'product_changed' => array('changed'),
      'product_created' => array('created'),
      'product_status' => array('status'),
    ),
  );

  return $schema;
}

3. Перейдем к product.module. В таблице я создал колонку статус, в которой будет хранится статус продукта, всего у меня 3 статуса, для каждого из статусов создаем константу:

/**
 * Product is pending.
 */
define('PRODUCT_PENDING', 0);

/**
 * Product is completed.
 */
define('PRODUCT_COMPLETED', 1);

/**
 * Product is canceled.
 */
define('PRODUCT_CANCELED', 2);

4. Описываем информацию об нашей сущности в хуке hook_entity_info():

/**
 * Implements hook_entity_info().
 */
function product_entity_info() {
  $return = array(
    // Ключ 'product' - машинное название сущности.
    'product' => array(
      'label' => t('Product'), // Человеко-понятное название сущности.
      'entity class' => 'Entity', // Класс сущности.
      'controller class' => 'EntityAPIController', // Контроллер сущности.
      'base table' => 'product', // Таблица, в которой хранится информация об сущностях.
      'uri callback' => 'product_uri', // Функция, которая возваращает uri сущности.
      'fieldable' => TRUE, // Позволяем прикреплять поля к сущности.
      'entity keys' => array('id' => 'id'),
      // Массив, в котором описываются типы сущности Product (не знаю как правильно выразится),
      // если привести анологию с модулем Node, то это типы материалов.
      'bundles' => array(
        // Ключ 'product' - машинное название типа.
        'product' => array(
          'label' => t('Product'), // Человеко-понятное название типа.
          'admin' => array(
            'path' => 'admin/config/product/products', // Путь, по которому доступна админка.
            'access arguments' => array('configure products settings'), // Права доступа в админку.
          ),
        ),
      ),
      // Режимы отображения сущности.
      'view modes' => array(
        // Ключи 'full' и 'administrator'  - машинные названия режимов.
        'full' => array(
          'label' => t('Full'), // Человеко-понятное название режима.
          'custom settings' => TRUE, // Режим включен по умолчанию.
        ),
        'administrator' => array(
          'label' => t('Administrator'),
          'custom settings' => TRUE,
        ),
      ),
      'module' => 'product',
    ),
  );

  return $return;
}

5. Создаем функцию product_uri(), которая возвращает uri сущности:

/**
 * Implements callback_entity_info_uri().
 */
function product_uri($product) {
  return array(
    'path' => 'product/' . $product->id,
  );
}

6. Создаем права доступа, которые нам потребуются в дальнейшем, для этого имплементируем хук hook_permission():

/**
 * Implements hook_permission().
 */
function product_permission() {
  return array(
    'configure products settings' => array(
      'title' => t('Configure products settings'),
      'description' => t('Allows users to configure products settings.'),
      'restrict access' => TRUE,
    ),
    'administer products' => array(
      'title' => t('Administer products'),
      'restrict access' => TRUE,
    ),
    'edit any products' => array(
      'title' => t('Edit any product'),
      'restrict access' => TRUE,
    ),
    'edit own products' => array(
      'title' => t('Edit own products'),
      'restrict access' => TRUE,
    ),
    'delete any products' => array(
      'title' => t('Delete any products'),
      'restrict access' => TRUE,
    ),
    'delete own products' => array(
      'title' => t('Delete own products'),
      'restrict access' => TRUE,
    ),
    'create products' => array(
      'title' => t('Create new products'),
    ),
    'view products' => array(
      'title' => t('View products'),
    ),
  );
}

7. Создаем функцию product_access(), которая будет проверять, может ли пользователь совершать какие либо операции над сущностью:

/**
 * Determines whether the current user may perform the operation on the product.
 *
 * @param $op
 *   The operation to be performed on the product. Possible values are:
 *   - "view"
 *   - "update"
 *   - "delete"
 *   - "create"
 * @param $entity_type
 *   The entity type on which the operation is to be perform.
 * @param $product
 *   The product object on which the operation is to be performed.
 * @param $account
 *   Optional, a user object representing the user for whom the operation is to
 *   be performed. Determines access for a user other than the current user.
 *
 * @return bool
 *   TRUE if the operation may be performed, FALSE otherwise.
 */
function product_access($op, $entity_type, $product = NULL, $account = NULL) {

  $rights = &drupal_static(__FUNCTION__, array());

  if (!in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
    // Если $op не равен ни одной из поддерживаемых операций, возвращаем "доступ запрещен".
    return FALSE;
  }

  // Если в функцию не передан пользователь, то проверяем права для текущего пользователя.
  if (empty($account)) {
    global $user;
    $account = $user;
  }

  // $product может быть объектом или не существовать, поэтому испольем его id,
  // или $entity_type в качестве статичного идентификатора, который будет использоваться ключом кеша.
  $cid = is_object($product) && !empty($product->id) ? $product->id : $entity_type;

  // Если мы уже проверяли для данной сущности и пользователя права доступа,
  // то возвращаем их из кеша.
  if (isset($rights[$account->uid][$cid][$op])) {
    return $rights[$account->uid][$cid][$op];
  }
  
  // Проверяем, может ли пользователь создавать новые продукты.
  if ($op == 'create' && user_access('create products', $account)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
  }

  if ($op == 'update') {
    // Проверяем, может ли пользователь редактирвоать любые продукты.
    if (user_access('edit any products', $account)) {
      $rights[$account->uid][$cid][$op] = TRUE;
      return TRUE;
    }
    // Проверяем, может ли пользователь редактирвоать свои продукты.
    elseif (user_access('edit own products', $account) && $product->uid == $account->uid) {
      $rights[$account->uid][$cid][$op] = TRUE;
      return TRUE;
    }
  }
  elseif ($op == 'delete') {
    // Проверяем, может ли пользователь удалять любые продукты.
    if (user_access('delete any products', $account)) {
      $rights[$account->uid][$cid][$op] = TRUE;
      return TRUE;
    }
    // Проверяем, может ли пользователь удалять свои продукты.
    elseif (user_access('delete own products', $account) && $product->uid == $account->uid) {
      $rights[$account->uid][$cid][$op] = TRUE;
      return TRUE;
    }
  }
  // Проверяем, может ли пользователь просматривать продукты.
  elseif ($op == 'view' && user_access('view products', $account)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
  }

  return FALSE;
}

8. Создаем функции, которые будут загружать сущность из базы данных:

/**
 * Loads a product by ID.
 */
function product_load($product_id) {
  $products = product_load_multiple(array($product_id), array());
  return $products ? reset($products) : FALSE;
}

/**
 * Loads multiple products by ID or based on a set of matching conditions.
 *
 * @see entity_load()
 *
 * @param $product_ids
 *   An array of product IDs.
 * @param $conditions
 *   An array of conditions to filter loaded products by on the {product}
 *   table in the form 'field' => $value.
 * @param $reset
 *   Whether to reset the internal product loading cache.
 *
 * @return
 *   An array of product objects indexed by product_id.
 */
function product_load_multiple($product_ids = array(), $conditions = array(), $reset = FALSE) {
  return entity_load('product', $product_ids, $conditions, $reset);
}

/**
 * Implements hook_entity_load().
 */
function product_entity_load($entities, $type) {
  // По умолчанию, сущность имеет только uid пользователя, который создал ее,
  // добавляем этот код, чтобы получить еще и имя пользователя.
  if ($type == 'product') {
    foreach ($entities as $entity) {
      if ($entity->uid) {
        if ($account = user_load($entity->uid)) {
          $entity->name = $account->name;
        }
      }
      else {
        $entity->name = variable_get('anonymous', t('Anonymous'));
      }
    }
  }
}

9. Создаем функции, которые будут удалять сущность из базы данных:

/**
 * Deletes a product.
 *
 * @param $product_id
 *   A product ID.
 */
function product_delete($product_id) {
  product_delete_multiple(array($product_id));
}

/**
 * Deletes multiple products.
 *
 * @param $product_ids
 *   An array of product IDs.
 */
function product_delete_multiple($product_ids) {
  entity_delete_multiple('product', $product_ids);
}

10. Создаем функцию темизации сущности, для этого имплементируем hook_theme():

/**
 * Implements hook_theme().
 */
function product_theme() {
  return array(
    'product' => array(
      'render element' => 'elements',
      'template' => 'templates/product',
    ),
  );
}

/**
 * Processes variables for product.tpl.php
 */
function template_preprocess_product(&$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];

  $variables['product'] = $variables['elements']['#product'];
  $product = $variables['product'];

  $variables['date'] = format_date($product->created);
  $variables['name'] = theme('username', array('account' => $product));

  $uri = product_uri($product);

  $variables['product_url']  = url($uri['path']);
  $variables['title'] = check_plain($product->title);
  $variables['page'] = $variables['view_mode'] == 'full';

  $variables = array_merge((array) $product, $variables);

  $variables += array('content' => array());
  foreach (element_children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }

  // Делаем поля доступными в качестве переменных для соответствующего языка.
  field_attach_preprocess('product', $product, $variables['content'], $variables);

  $variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['name'], '!datetime' => $variables['date']));

  $variables['title_attributes_array']['class'][] = 'product-title';

  if ($variables['status'] == PRODUCT_PENDING) {
    $variables['classes_array'][] = 'product-pending';
  }
  elseif ($variables['status'] == PRODUCT_COMPLETED) {
    $variables['classes_array'][] = 'product-completed';
  }
  elseif ($variables['status'] == PRODUCT_CANCELED) {
    $variables['classes_array'][] = 'product-canceled';
  }
}

11. Создаем функции, которые будут выводить сущности:

/**
 * Constructs a drupal_render() style array from an array of loaded products.
 *
 * @param $products
 *   An array of products as returned by product_load_multiple().
 * @param $view_mode
 *   View mode, e.g. 'teaser', 'full'...
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to NULL which is
 *   the global content language of the current request.
 *
 * @return array
 *   An array in the format expected by drupal_render().
 */
function product_view_multiple($products, $view_mode = 'full', $langcode = NULL) {
  // Подготавливаем данные для отображения.
  field_attach_prepare_view('product', $products, $view_mode, $langcode);
  entity_prepare_view('product', $products, $langcode);
  $build = array();
  $weight = 0;
  foreach ($products as $product) {
    $build['products'][$product->id] = product_view($product, $view_mode, $langcode);
    $build['products'][$product->id]['#weight'] = $weight++;
  }
  $build['products']['#sorted'] = TRUE;
  return $build;
}

/**
 * Generates an array for rendering the given product.
 *
 * @param $product
 *   A product object.
 * @param $view_mode
 *   View mode, e.g. 'teaser', 'full'...
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
 *
 * @return array
 *   An array as expected by drupal_render().
 */
function product_view($product, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    global $language;
    $langcode = $language->language;
  }

  // Заполняем $product->content данными в виде рендерного массива.
  product_build_content($product, $view_mode, $langcode);

  $build = $product->content;
  // Скрываем данные, чтобы избежать их дублирования при выводе $product->content.
  unset($product->content);

  $build += array(
    '#theme' => 'product',
    '#product' => $product,
    '#view_mode' => $view_mode,
    '#language' => $langcode,
  );

  return $build;
}

/**
 * Builds a structured array representing the product's content.
 *
 * @param $product
 *   A product object.
 * @param $view_mode
 *   View mode, e.g. 'teaser', 'full'...
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
 */
function product_build_content($product, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    global $language;
    $langcode = $language->language;
  }

  // Удаляем существующий конент, если существует.
  $product->content = array();

  field_attach_prepare_view('product', array($product->id => $product), $view_mode, $langcode);
  entity_prepare_view('product', array($product->id => $product), $langcode);
  $product->content += field_attach_view('product', $product, $view_mode, $langcode);

  $product->content += array('#view_mode' => $view_mode);

  $product->content['links'] = array(
    '#theme' => 'links__product',
    '#pre_render' => array('drupal_pre_render_links'),
    '#attributes' => array('class' => array('links')),
  );
}

12. В hook_menu() создаем страницы, которые потребуются для работы модуля:

/**
 * Implements hook_menu().
 */
function product_menu() {
  // Top level "Product" container.
  $items['admin/config/product'] = array(
    'title' => 'Product',
    'description' => 'Administration tools.',
    'page callback' => 'system_admin_menu_block_page',
    'access arguments' => array('access administration pages'),
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/config/product/products'] = array(
    'title' => 'Products',
    'description' => 'Configure general product settings, fields, and displays.',
    'page callback' => 'product_admin_products',
    'access arguments' => array('administer products'),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'product.admin.inc',
  );
  $items['admin/config/product/products/list'] = array(
    'title' => 'Products',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  // Страница создания продукта.
  $items['product/add'] = array(
    'title' => 'Add product',
    'page callback' => 'product_page_add',
    'access callback' => 'product_access',
    'access arguments' => array('create', 'product'),
    'file' => 'product.pages.inc',
  );
  // Страница просмотра продукта.
  $items['product/%product'] = array(
    'title callback' => 'product_page_title',
    'title arguments' => array(1),
    'page callback' => 'product_page_view',
    'page arguments' => array(1),
    'access callback' => 'product_access',
    'access arguments' => array('view', 'product', 1),
    'file' => 'product.pages.inc',
  );
  $items['product/%product/view'] = array(
    'title' => 'View',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => 0,
  );
  // Страница редактирования продукта.
  $items['product/%product/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'product_page_edit',
    'page arguments' => array(1),
    'access callback' => 'product_access',
    'access arguments' => array('update', 'product', 1),
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
    'file' => 'product.pages.inc',
  );
  // Страница удаления продукта.
  $items['product/%product/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('product_delete_confirm', 1),
    'access callback' => 'product_access',
    'access arguments' => array('delete', 'product', 1),
    'file' => 'product.pages.inc',
  );

  return $items;
}

13. С помощью hook_admin_paths(), говорим системе, какие страницы являются административными:

/**
 * Implements hook_admin_paths().
 */
function product_admin_paths() {
  $paths = array(
    'product/add' => TRUE,
    'product/*/edit' => TRUE,
    'product/*/delete' => TRUE,
  );
  return $paths;
}

14. Последней функцией в product.module будет product_status_get_title(), которая по id статуса продукта будет возвращать человеко-понятное название статуса:

/**
 * Returns the human readable title of any or all product statuses.
 *
 * @param $id
 *   Optional parameter specifying the id of the product status whose title
 *     to return.
 *
 * @return mixed
 *   Either an array of all product status titles keyed by the status_id or a
 *     string containing the human readable title for the specified status.
 */
function product_status_get_title($id = NULL) {
  $ids = array(
    PRODUCT_PENDING => t('Pending'),
    PRODUCT_COMPLETED => t('Completed'),
    PRODUCT_CANCELED => t('Canceled'),
  );

  if (isset($id)) {
    return $ids[$id];
  }

  return $ids;
}

15. С product.module закончили, теперь переходим к product.pages.inc, в этом файле создадим все колбеки для страниц. Создаем функции, которые будут показывать сущность по адресу product/%product:

/**
 * Title callback: Returns the title of the product.
 *
 * @param $product
 *   The product object.
 *
 * @return
 *   An unsanitized string that is the title of the product.
 *
 * @see product_menu()
 */
function product_page_title($product) {
  return $product->title;
}

/**
 * Menu callback: Displays a single product.
 *
 * @param $product
 *   The product object.
 *
 * @return array
 *   A page array suitable for use by drupal_render().
 *
 * @see product_menu()
 */
function product_page_view($product) {
  // For markup consistency with other pages, use product_view_multiple() rather than product_view().
  $products = product_view_multiple(array($product->id => $product), 'full');

  return $products;
}

16. Создаем функцию product_page_add(), которая будет выводить форму создания сущности по адресу product/add:

/*
 * Create a basic entity structure to be used and passed to the validation
 * and submission functions.
 */
function product_page_add() {
  global $user;
  $name = variable_get('anonymous', t('Anonymous'));
  if ($user->uid && $account = user_load($user->uid)) {
    $name = $account->name;
  }

  $product = entity_create('product', array('uid' => $user->uid, 'name' => $name));
  return drupal_get_form('product_form', $product, 'create');
}

17. Создаем функцию product_page_edit(), которая будет показывать форму редактирования сущности, по адресу product/%product/edit:

/*
 * Presents the product editing form.
 */
function product_page_edit($product) {
  drupal_set_title(t('<em>Edit</em> @title', array('@title' => $product->title)), PASS_THROUGH);
  return drupal_get_form('product_form', $product);
}

18. Как вы могли заметить, для создания и редактирвоания сущности у меня используется одна и таже форма product_form, создаем функцию, формирующую эту форму:

/**
 * Form constructor for the product add/edit form.
 *
 * @see product_form_validate()
 * @see product_form_submit()
 * @see product_form_delete_submit()
 * @ingroup forms
 */
function product_form($form, &$form_state, $product, $op = 'update') {
  $form['#attributes']['class'][] = 'product-form';

  $form['product'] = array(
    '#type' => 'value',
    '#value' => $product,
  );

  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Title'),
    '#default_value' => !empty($product->title) ? $product->title : '',
    '#required' => TRUE,
  );

  $form['additional_settings'] = array(
    '#type' => 'vertical_tabs',
    '#weight' => 99,
  );

  // Информация об авторе продукта.
  $form['author'] = array(
    '#type' => 'fieldset',
    '#access' => user_access('administer products'),
    '#title' => t('Authoring information'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#group' => 'additional_settings',
    '#attributes' => array(
      'class' => array('product-form-author'),
    ),
    '#attached' => array(
      'js' => array(
        drupal_get_path('module', 'product') . '/js/product-fieldset-summaries.js',
        array(
          'type' => 'setting',
          'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))),
        ),
      ),
    ),
    '#weight' => 90,
  );
  $form['author']['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Authored by'),
    '#maxlength' => 60,
    '#autocomplete_path' => 'user/autocomplete',
    '#default_value' => !empty($product->name) ? $product->name : '',
    '#weight' => -1,
    '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
  );
  $form['author']['date'] = array(
    '#type' => 'textfield',
    '#title' => t('Authored on'),
    '#maxlength' => 25,
    '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($product->created) ? format_date($product->created, 'custom', 'Y-m-d H:i:s O') : format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($product->created) ? format_date($product->created, 'custom', 'O') : format_date(REQUEST_TIME, 'custom', 'O'))),
    '#default_value' => !empty($product->created) ? format_date($product->created, 'custom', 'Y-m-d H:i:s O') : '',
  );

  // Статусы продукта.
  $form['options'] = array(
    '#type' => 'fieldset',
    '#access' => user_access('administer products'),
    '#title' => t('Product status'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#group' => 'additional_settings',
    '#attributes' => array(
      'class' => array('product-form-options'),
    ),
    '#attached' => array(
      drupal_get_path('module', 'product') . '/js/product-fieldset-summaries.js',
    ),
    '#weight' => 95,
  );
  $form['options']['status'] = array(
    '#type' => 'radios',
    '#title' => t('Status'),
    '#default_value' => isset($product->status) ? $product->status : PRODUCT_PENDING,
    '#options' => array(
      PRODUCT_PENDING => product_status_get_title(PRODUCT_PENDING),
      PRODUCT_COMPLETED => product_status_get_title(PRODUCT_COMPLETED),
      PRODUCT_CANCELED => product_status_get_title(PRODUCT_CANCELED),
    ),
  );

  // Кнопка отправки формы.
  $form['actions'] = array('#type' => 'actions');
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
    '#weight' => 5,
  );
  
  // Если пользователь редактирует продукт и у него есть разрешение для удаления продукта,
  // то добавляем кнопку "Удалить".
  if ($op == 'update' && product_access('delete', 'product', $product)) {
    $form['actions']['delete'] = array(
      '#type' => 'submit',
      '#value' => t('Delete'),
      '#weight' => 15,
      '#submit' => array('product_form_delete_submit'),
    );
  }
  
  // Прикрепляем все созданные поля сущности к форме.
  field_attach_form('product', $product, $form, $form_state);

  return $form;
}

/**
 * Form validation handler for product_form().
 *
 * @see product_form()
 * @see product_form_submit()
 */
function product_form_validate($form, &$form_state) {
  $product = $form_state['values']['product'];
  
  // Проверяем корректность автора.
  $product->uid = 0;
  if (!empty($form_state['values']['name'])) {
    if ($account = user_load_by_name($form_state['values']['name'])) {
      $product->uid = $account->uid;
    }
    elseif ($form_state['values']['name'] != variable_get('anonymous', t('Anonymous'))) {
      form_set_error('name', t('The username %name does not exist.', array('%name' => $form_state['values']['name'])));
    }
  }

  // Проверяем корректность даты публикации.
  if (!empty($form_state['values']['date'])) {
    if (strtotime($form_state['values']['date']) === FALSE) {
      form_set_error('date', t('You have to specify a valid date.'));
    }
    else {
      $date = date_create($form_state['values']['date']);
      $product->created = $date->getTimestamp();
    }
  }
  else {
    $product->created = REQUEST_TIME;
  }
}

/**
 * Form submission handler for product_form().
 *
 * @see product_form()
 * @see product_form_validate()
 */
function product_form_submit($form, &$form_state) {
  $product = $form_state['values']['product'];
  $product->title = $form_state['values']['title'];
  $product->changed = REQUEST_TIME;
  $product->status = $form_state['values']['status'];

  field_attach_submit('product', $product, $form, $form_state);

  $is_new = !empty($product->is_new) ? TRUE : FALSE;
  $product->save();

  if ($is_new) {
    drupal_set_message(t('%title has been created.', array('%title' => $product->title)));
  }
  else {
    drupal_set_message(t('%title has been updated.', array('%title' => $product->title)));
  }

  $form_state['redirect'] = array('product/' . $product->id);
}

/**
 * Form submission handler for product_form().
 *
 * Handles the 'Delete' button on the product form.
 *
 * @see product_form()
 * @see product_form_validate()
 */
function product_form_delete_submit($form, &$form_state) {
  $destination = array();
  if (isset($_GET['destination'])) {
    $destination = drupal_get_destination();
    unset($_GET['destination']);
  }
  $product = $form_state['values']['product'];
  $form_state['redirect'] = array('product/' . $product->id . '/delete', array('query' => $destination));
}

19. Создаем форму удаления сущности:

/**
 * Form constructor for the product deletion confirmation form.
 *
 * @see product_delete_confirm_submit()
 */
function product_delete_confirm($form, &$form_state, $product) {
  $form['product_id'] = array('#type' => 'value', '#value' => $product->id);
  return confirm_form($form,
    t('Are you sure you want to delete %title?', array('%title' => $product->title)),
    'products',
    t('This action cannot be undone.'),
    t('Delete'),
    t('Cancel')
  );
}

/**
 * Executes product deletion.
 *
 * @see product_delete_confirm()
 */
function product_delete_confirm_submit($form, &$form_state) {
  if ($form_state['values']['confirm']) {
    $product = product_load($form_state['values']['product_id']);
    product_delete($form_state['values']['product_id']);
    drupal_set_message(t('%title has been deleted.', array('%title' => $product->title)));
  }

  $form_state['redirect'] = 'admin/config/product/products';
}

20. С product.pages.inc закончили, теперь переходим к product.admin.inc, в этом файле будет одна функция product_admin_products(), которая будет выводить список всех созданных продуктов по адресу admin/config/product/products:

/**
 * Page callback: Builds the product administration overview.
 */
function product_admin_products() {

  // Формируем сортируемую шапку для таблицы.
  $header = array(
    'title' => array('data' => t('Title'), 'field' => 'p.title'),
    'author' => t('Author'),
    'status' => array('data' => t('Status'), 'field' => 'p.status'),
    'changed' => array('data' => t('Updated'), 'field' => 'p.changed', 'sort' => 'desc'),
    'operations' => array('data' => t('Operations')),
  );

  $query = db_select('product', 'p')->extend('PagerDefault')->extend('TableSort');
  $product_ids = $query->fields('p', array('id'))
    ->limit(50)
    ->orderByHeader($header)
    ->execute()
    ->fetchCol();

  $products = product_load_multiple($product_ids);

  // Подготавливаем список продуктов.
  $destination = drupal_get_destination();
  $rows = array();
  foreach ($products as $product) {
    $rows[$product->id] = array(
      'title' => array(
        'data' => array(
          '#type' => 'link',
          '#title' => $product->title,
          '#href' => 'product/' . $product->id,
        ),
      ),
      'author' => theme('username', array('account' => $product)),
      'status' => product_status_get_title($product->status),
      'changed' => format_date($product->changed, 'short'),
    );

    // Формируем список доступных операций над текущим продуктом.
    $operations = array();
    if (product_access('update', 'product', $product)) {
      $operations['edit'] = array(
        'title' => t('edit'),
        'href' => 'product/' . $product->id . '/edit',
        'query' => $destination,
      );
    }
    if (product_access('delete', 'product', $product)) {
      $operations['delete'] = array(
        'title' => t('delete'),
        'href' => 'product/' . $product->id . '/delete',
        'query' => $destination,
      );
    }

    $rows[$product->id]['operations'] = array();
    if (count($operations) > 1) {
      // Выводим операции в виде списка.
      $rows[$product->id]['operations'] = array(
        'data' => array(
          '#theme' => 'links__product_operations',
          '#links' => $operations,
          '#attributes' => array('class' => array('links', 'inline')),
        ),
      );
    }
    elseif (!empty($operations)) {
      // Выводим первую и единственную операцию.
      $link = reset($operations);
      $rows[$product->id]['operations'] = array(
        'data' => array(
          '#type' => 'link',
          '#title' => $link['title'],
          '#href' => $link['href'],
          '#options' => array('query' => $link['query']),
        ),
      );
    }
  }

  $page = array();
  $page['products'] = array(
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => $rows,
    '#empty' => t('No products available.'),
  );

  $page['pager'] = array('#markup' => theme('pager'));
  return $page;
}

21. Теперь переходим к шаблону product.tpl.php:

<div id="product-<?php print $product->id; ?>" class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>

  <?php print render($title_prefix); ?>
  <?php if (!$page): ?>
    <h2<?php print $title_attributes; ?>><?php print $title; ?></h2>
  <?php endif; ?>
  <?php print render($title_suffix); ?>

  <div class="submitted">
    <?php print $submitted; ?>
  </div>

  <div class="content"<?php print $content_attributes; ?>>
    <?php print render($content); ?>
  </div>

</div>

21. Осталось дело за малым, если помните, в форме создания/редактирования продукта мы сделали вериткальные вкладки, поэтому в файл product-fieldset-summaries.js добавляем несколько строк кода для украшения этих вкладок:

(function ($) {
  Drupal.behaviors.productFieldsetSummaries = {
    attach: function (context) {
      $('fieldset.product-form-author', context).drupalSetSummary(function (context) {
        return $('div.form-item-name input', context).val();
      });

      $('fieldset.product-form-author', context).drupalSetSummary(function (context) {
        var name = $('div.form-item-name input', context).val() || Drupal.settings.anonymous,
          date = $('div.form-item-date input', context).val();

        return date ?
          Drupal.t('By @name on @date', { '@name': name, '@date': date }) :
          Drupal.t('By @name', { '@name': name });
      });

      $('fieldset.product-form-options', context).drupalSetSummary(function (context) {
        var vals = [];
        var statusId = $('div.form-item-status input:checked', context).attr('id');
        var status = $('div.form-item-status label[for=' + statusId + ']', context).text();

        vals.push(Drupal.checkPlain($.trim(status)));
        return vals.join(', ');
      });
    }
  };
})(jQuery);

Вот и все. Так же читайте как связать сущность с модулем Views.

Benya