Drupal

エンタープライズ向けの高機能CMS。複雑な要件に対応可能な柔軟性とスケーラビリティを提供。

CMSオープンソースエンタープライズPHPSymfony
ライセンス
GPL v2
言語
PHP
料金
完全無料(自己ホスト)

CMS

Drupal

概要

Drupalは、2001年に誕生したオープンソースのエンタープライズ向けコンテンツ管理システムです。Symfonyフレームワークをベースに構築され、高度な拡張性とスケーラビリティを提供します。政府機関、大学、大企業など、セキュリティと柔軟性を重視する組織で広く採用されています。

詳細

Drupalは「ディストリビューション」と呼ばれる特定用途向けのパッケージを提供し、コマースサイト、政府サイト、教育機関向けサイトなど、様々な用途に最適化された構成で利用できます。

主な特徴:

  • モジュラーアーキテクチャ: 45,000以上のモジュール(拡張機能)
  • 多言語対応: 100以上の言語に対応、多言語サイトの構築が容易
  • 強力なコンテンツモデリング: フィールドAPI、ビュー、コンテンツタイプ
  • エンタープライズ機能: ワークフロー、リビジョン管理、権限管理
  • API First: RESTful API、JSON:API標準サポート
  • Symfonyベース: モダンなPHPフレームワークによる堅牢な基盤
  • キャッシング: 高度なキャッシング機構による高速化

メリット・デメリット

メリット

  • エンタープライズレベルのセキュリティ
  • 高度にカスタマイズ可能なコンテンツ構造
  • 強力な権限管理システム
  • 優れたパフォーマンスとスケーラビリティ
  • 活発な開発者コミュニティ
  • 包括的なAPIサポート
  • 多言語・多地域対応
  • バージョン管理とワークフロー

デメリット

  • 学習曲線が急峻
  • 初期設定が複雑
  • 小規模サイトには過剰な機能
  • テーマ開発に専門知識が必要
  • アップグレードが困難な場合がある
  • ホスティング要件が高い

参考ページ

書き方の例

1. Hello World(基本的なセットアップ)

モジュールの基本構造(hello_world.info.yml)

name: 'Hello World'
type: module
description: 'Hello World モジュールの例'
package: 'Custom'
core_version_requirement: ^10
dependencies:
  - drupal:system (>=10.0)

基本的なコントローラー(src/Controller/HelloController.php)

<?php

namespace Drupal\hello_world\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

/**
 * Hello World コントローラー
 */
class HelloController extends ControllerBase {

  /**
   * ロガーファクトリー
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * コンストラクタ
   */
  public function __construct(LoggerChannelFactoryInterface $logger_factory) {
    $this->loggerFactory = $logger_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('logger.factory')
    );
  }

  /**
   * Hello World ページ
   *
   * @return array
   *   レンダー配列
   */
  public function hello() {
    $this->loggerFactory->get('hello_world')->notice('Hello page accessed');
    
    return [
      '#type' => 'markup',
      '#markup' => $this->t('Hello, World! Welcome to Drupal @version', [
        '@version' => \Drupal::VERSION
      ]),
      '#cache' => [
        'max-age' => 0,
      ],
    ];
  }
}

2. テーマ開発

テーマ情報ファイル(mytheme.info.yml)

name: 'My Custom Theme'
type: theme
description: 'カスタムDrupalテーマ'
package: Custom
core_version_requirement: ^10
base theme: stable9

libraries:
  - mytheme/global

regions:
  header: 'Header'
  primary_menu: 'Primary menu'
  secondary_menu: 'Secondary menu'
  breadcrumb: 'Breadcrumb'
  help: 'Help'
  highlighted: 'Highlighted'
  content: 'Content'
  sidebar_first: 'Sidebar first'
  sidebar_second: 'Sidebar second'
  footer: 'Footer'

テーマのテンプレート(templates/page.html.twig)

{#
/**
 * @file
 * デフォルトのページテンプレート
 */
#}
<!DOCTYPE html>
<html{{ html_attributes }}>
  <head>
    <head-placeholder token="{{ placeholder_token }}">
    <title>{{ head_title|safe_join(' | ') }}</title>
    <css-placeholder token="{{ placeholder_token }}">
    <js-placeholder token="{{ placeholder_token }}">
  </head>
  <body{{ attributes }}>
    <a href="#main-content" class="visually-hidden focusable">
      {{ 'Skip to main content'|t }}
    </a>
    {{ page_top }}
    {{ page }}
    {{ page_bottom }}
    <js-bottom-placeholder token="{{ placeholder_token }}">
  </body>
</html>

リージョンのテンプレート(templates/region.html.twig)

{#
/**
 * @file
 * リージョンのデフォルトテンプレート
 */
#}
{% if content %}
  <div{{ attributes.addClass('region', 'region-' ~ region|clean_class) }}>
    {{ content }}
  </div>
{% endif %}

3. モジュール開発

カスタムブロックモジュール(src/Plugin/Block/CustomBlock.php)

<?php

namespace Drupal\my_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
 * カスタムブロックの提供
 *
 * @Block(
 *   id = "my_module_custom_block",
 *   admin_label = @Translation("Custom Block"),
 *   category = @Translation("Custom")
 * )
 */
class CustomBlock extends BlockBase implements BlockPluginInterface, ContainerFactoryPluginInterface {

  /**
   * エンティティタイプマネージャー
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * コンストラクタ
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    EntityTypeManagerInterface $entity_type_manager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $config = $this->getConfiguration();
    
    // 最近のコンテンツを取得
    $node_storage = $this->entityTypeManager->getStorage('node');
    $query = $node_storage->getQuery()
      ->condition('status', 1)
      ->condition('type', $config['content_type'])
      ->sort('created', 'DESC')
      ->range(0, $config['item_count'])
      ->accessCheck(TRUE);
    
    $nids = $query->execute();
    $nodes = $node_storage->loadMultiple($nids);
    
    $items = [];
    foreach ($nodes as $node) {
      $items[] = [
        'title' => $node->getTitle(),
        'url' => $node->toUrl(),
        'created' => $node->getCreatedTime(),
      ];
    }
    
    return [
      '#theme' => 'my_module_custom_block',
      '#items' => $items,
      '#title' => $config['block_title'],
      '#cache' => [
        'tags' => ['node_list'],
        'contexts' => ['languages:language_interface'],
        'max-age' => 300,
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form = parent::blockForm($form, $form_state);
    $config = $this->getConfiguration();
    
    $form['block_title'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Block Title'),
      '#default_value' => $config['block_title'] ?? '',
    ];
    
    $form['content_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Content Type'),
      '#options' => $this->getContentTypeOptions(),
      '#default_value' => $config['content_type'] ?? 'article',
    ];
    
    $form['item_count'] = [
      '#type' => 'number',
      '#title' => $this->t('Number of items'),
      '#default_value' => $config['item_count'] ?? 5,
      '#min' => 1,
      '#max' => 20,
    ];
    
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    parent::blockSubmit($form, $form_state);
    $this->configuration['block_title'] = $form_state->getValue('block_title');
    $this->configuration['content_type'] = $form_state->getValue('content_type');
    $this->configuration['item_count'] = $form_state->getValue('item_count');
  }

  /**
   * コンテンツタイプのオプションを取得
   */
  protected function getContentTypeOptions() {
    $options = [];
    $types = $this->entityTypeManager
      ->getStorage('node_type')
      ->loadMultiple();
    
    foreach ($types as $type) {
      $options[$type->id()] = $type->label();
    }
    
    return $options;
  }
}

4. カスタム投稿タイプ(コンテンツタイプ)

カスタムエンティティの定義(src/Entity/Product.php)

<?php

namespace Drupal\my_module\Entity;

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\user\UserInterface;

/**
 * 製品エンティティを定義
 *
 * @ContentEntityType(
 *   id = "product",
 *   label = @Translation("Product"),
 *   label_collection = @Translation("Products"),
 *   handlers = {
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\my_module\ProductListBuilder",
 *     "form" = {
 *       "default" = "Drupal\my_module\Form\ProductForm",
 *       "add" = "Drupal\my_module\Form\ProductForm",
 *       "edit" = "Drupal\my_module\Form\ProductForm",
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
 *     },
 *   },
 *   base_table = "product",
 *   admin_permission = "administer product entities",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "langcode" = "langcode",
 *   },
 *   links = {
 *     "canonical" = "/product/{product}",
 *     "add-form" = "/admin/content/product/add",
 *     "edit-form" = "/admin/content/product/{product}/edit",
 *     "delete-form" = "/admin/content/product/{product}/delete",
 *     "collection" = "/admin/content/product",
 *   },
 *   field_ui_base_route = "entity.product.settings"
 * )
 */
class Product extends ContentEntityBase implements ContentEntityInterface {

  use EntityChangedTrait;

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Name'))
      ->setDescription(t('The name of the Product.'))
      ->setSettings([
        'max_length' => 255,
        'text_processing' => 0,
      ])
      ->setDefaultValue('')
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ])
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => -4,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE)
      ->setRequired(TRUE);

    $fields['description'] = BaseFieldDefinition::create('text_long')
      ->setLabel(t('Description'))
      ->setDescription(t('The description of the Product.'))
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'text_default',
        'weight' => 0,
      ])
      ->setDisplayOptions('form', [
        'type' => 'text_textarea',
        'weight' => 0,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['price'] = BaseFieldDefinition::create('decimal')
      ->setLabel(t('Price'))
      ->setDescription(t('The price of the Product.'))
      ->setSettings([
        'precision' => 10,
        'scale' => 2,
      ])
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'number_decimal',
        'weight' => -3,
      ])
      ->setDisplayOptions('form', [
        'type' => 'number',
        'weight' => -3,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['sku'] = BaseFieldDefinition::create('string')
      ->setLabel(t('SKU'))
      ->setDescription(t('The SKU of the Product.'))
      ->setSettings([
        'max_length' => 64,
        'text_processing' => 0,
      ])
      ->setDefaultValue('')
      ->setDisplayOptions('view', [
        'label' => 'above',
        'type' => 'string',
        'weight' => -2,
      ])
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => -2,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE)
      ->addConstraint('UniqueField');

    $fields['status'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Published'))
      ->setDescription(t('A boolean indicating whether the Product is published.'))
      ->setDefaultValue(TRUE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
        'weight' => 10,
      ]);

    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created'))
      ->setDescription(t('The time that the entity was created.'));

    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the entity was last edited.'));

    return $fields;
  }
}

5. データベース操作

データベースクエリとトランザクション(DatabaseExample.php)

<?php

namespace Drupal\my_module\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * データベース操作の例
 */
class DatabaseExample {

  use StringTranslationTrait;

  /**
   * データベース接続
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * ロガーサービス
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * コンストラクタ
   */
  public function __construct(Connection $database, LoggerChannelFactoryInterface $logger_factory) {
    $this->database = $database;
    $this->logger = $logger_factory->get('my_module');
  }

  /**
   * データの挿入
   */
  public function insertData($name, $value) {
    try {
      $result = $this->database->insert('my_module_data')
        ->fields([
          'name' => $name,
          'value' => $value,
          'created' => \Drupal::time()->getRequestTime(),
        ])
        ->execute();
      
      return $result;
    }
    catch (\Exception $e) {
      $this->logger->error('Insert failed: @message', ['@message' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * データの取得
   */
  public function getData($limit = 10) {
    $query = $this->database->select('my_module_data', 'md')
      ->fields('md', ['id', 'name', 'value', 'created'])
      ->orderBy('created', 'DESC')
      ->range(0, $limit);
    
    return $query->execute()->fetchAll();
  }

  /**
   * 条件付きデータ取得
   */
  public function getDataByName($name) {
    $query = $this->database->select('my_module_data', 'md')
      ->fields('md')
      ->condition('name', $name, '=');
    
    return $query->execute()->fetchObject();
  }

  /**
   * データの更新
   */
  public function updateData($id, $name, $value) {
    try {
      $result = $this->database->update('my_module_data')
        ->fields([
          'name' => $name,
          'value' => $value,
          'changed' => \Drupal::time()->getRequestTime(),
        ])
        ->condition('id', $id, '=')
        ->execute();
      
      return $result;
    }
    catch (\Exception $e) {
      $this->logger->error('Update failed: @message', ['@message' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * データの削除
   */
  public function deleteData($id) {
    try {
      $result = $this->database->delete('my_module_data')
        ->condition('id', $id, '=')
        ->execute();
      
      return $result;
    }
    catch (\Exception $e) {
      $this->logger->error('Delete failed: @message', ['@message' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * トランザクション処理
   */
  public function complexOperation($data_array) {
    $transaction = $this->database->startTransaction();
    
    try {
      foreach ($data_array as $data) {
        $this->database->insert('my_module_data')
          ->fields([
            'name' => $data['name'],
            'value' => $data['value'],
            'created' => \Drupal::time()->getRequestTime(),
          ])
          ->execute();
      }
      
      // 統計情報の更新
      $this->database->merge('my_module_statistics')
        ->key(['date' => date('Y-m-d')])
        ->fields([
          'count' => count($data_array),
          'updated' => \Drupal::time()->getRequestTime(),
        ])
        ->expression('count', 'count + :inc', [':inc' => count($data_array)])
        ->execute();
      
      return TRUE;
    }
    catch (\Exception $e) {
      $transaction->rollBack();
      $this->logger->error('Transaction failed: @message', ['@message' => $e->getMessage()]);
      return FALSE;
    }
  }

  /**
   * 複雑なクエリの例
   */
  public function getStatistics() {
    $query = $this->database->select('my_module_data', 'md');
    $query->addExpression('COUNT(*)', 'total_count');
    $query->addExpression('COUNT(DISTINCT name)', 'unique_names');
    $query->addExpression('MAX(created)', 'last_created');
    $query->addExpression('MIN(created)', 'first_created');
    
    return $query->execute()->fetchObject();
  }

  /**
   * 結合クエリの例
   */
  public function getDataWithUser() {
    $query = $this->database->select('my_module_data', 'md');
    $query->join('users_field_data', 'u', 'md.uid = u.uid');
    $query->fields('md', ['id', 'name', 'value'])
      ->fields('u', ['name'])
      ->condition('u.status', 1)
      ->orderBy('md.created', 'DESC');
    
    return $query->execute()->fetchAll();
  }
}

6. API連携

REST APIリソースプラグイン(src/Plugin/rest/resource/ProductResource.php)

<?php

namespace Drupal\my_module\Plugin\rest\resource;

use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * 製品情報を提供するRESTリソース
 *
 * @RestResource(
 *   id = "product_resource",
 *   label = @Translation("Product Resource"),
 *   uri_paths = {
 *     "canonical" = "/api/product/{id}",
 *     "collection" = "/api/products"
 *   }
 * )
 */
class ProductResource extends ResourceBase {

  /**
   * エンティティタイプマネージャー
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * コンストラクタ
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    array $serializer_formats,
    LoggerInterface $logger,
    EntityTypeManagerInterface $entity_type_manager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->getParameter('serializer.formats'),
      $container->get('logger.factory')->get('my_module'),
      $container->get('entity_type.manager')
    );
  }

  /**
   * GETリクエストへの応答
   */
  public function get($id = NULL) {
    if ($id) {
      // 単一の製品を取得
      $product = $this->entityTypeManager
        ->getStorage('product')
        ->load($id);
      
      if (!$product) {
        throw new NotFoundHttpException('Product not found');
      }
      
      if (!$product->access('view')) {
        throw new AccessDeniedHttpException();
      }
      
      $response_data = [
        'id' => $product->id(),
        'name' => $product->get('name')->value,
        'description' => $product->get('description')->value,
        'price' => $product->get('price')->value,
        'sku' => $product->get('sku')->value,
        'created' => $product->get('created')->value,
        'changed' => $product->get('changed')->value,
      ];
      
      return new ResourceResponse($response_data);
    }
    else {
      // 製品一覧を取得
      $storage = $this->entityTypeManager->getStorage('product');
      $query = $storage->getQuery()
        ->condition('status', 1)
        ->sort('created', 'DESC')
        ->pager(10)
        ->accessCheck(TRUE);
      
      $ids = $query->execute();
      $products = $storage->loadMultiple($ids);
      
      $response_data = [];
      foreach ($products as $product) {
        if ($product->access('view')) {
          $response_data[] = [
            'id' => $product->id(),
            'name' => $product->get('name')->value,
            'price' => $product->get('price')->value,
            'sku' => $product->get('sku')->value,
          ];
        }
      }
      
      return new ResourceResponse($response_data);
    }
  }

  /**
   * POSTリクエストへの応答
   */
  public function post($data) {
    // 権限チェック
    if (!\Drupal::currentUser()->hasPermission('create product entities')) {
      throw new AccessDeniedHttpException();
    }
    
    // バリデーション
    if (empty($data['name']) || empty($data['price'])) {
      throw new BadRequestHttpException('Name and price are required');
    }
    
    // 製品エンティティの作成
    $product = $this->entityTypeManager
      ->getStorage('product')
      ->create([
        'name' => $data['name'],
        'description' => $data['description'] ?? '',
        'price' => $data['price'],
        'sku' => $data['sku'] ?? '',
      ]);
    
    try {
      $product->save();
      $this->logger->notice('Product created: @name', ['@name' => $product->get('name')->value]);
      
      return new ResourceResponse([
        'id' => $product->id(),
        'message' => 'Product created successfully',
      ], 201);
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to create product: @error', ['@error' => $e->getMessage()]);
      throw new ServiceUnavailableHttpException(NULL, 'Product creation failed');
    }
  }

  /**
   * PATCHリクエストへの応答
   */
  public function patch($id, $data) {
    $product = $this->entityTypeManager
      ->getStorage('product')
      ->load($id);
    
    if (!$product) {
      throw new NotFoundHttpException('Product not found');
    }
    
    if (!$product->access('update')) {
      throw new AccessDeniedHttpException();
    }
    
    // 更新可能なフィールドのみ更新
    $allowed_fields = ['name', 'description', 'price', 'sku'];
    foreach ($allowed_fields as $field) {
      if (isset($data[$field])) {
        $product->set($field, $data[$field]);
      }
    }
    
    try {
      $product->save();
      
      return new ResourceResponse([
        'id' => $product->id(),
        'message' => 'Product updated successfully',
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to update product: @error', ['@error' => $e->getMessage()]);
      throw new ServiceUnavailableHttpException(NULL, 'Product update failed');
    }
  }

  /**
   * DELETEリクエストへの応答
   */
  public function delete($id) {
    $product = $this->entityTypeManager
      ->getStorage('product')
      ->load($id);
    
    if (!$product) {
      throw new NotFoundHttpException('Product not found');
    }
    
    if (!$product->access('delete')) {
      throw new AccessDeniedHttpException();
    }
    
    try {
      $product->delete();
      
      return new ResourceResponse(NULL, 204);
    }
    catch (\Exception $e) {
      $this->logger->error('Failed to delete product: @error', ['@error' => $e->getMessage()]);
      throw new ServiceUnavailableHttpException(NULL, 'Product deletion failed');
    }
  }
}