Drupal
エンタープライズ向けの高機能CMS。複雑な要件に対応可能な柔軟性とスケーラビリティを提供。
CMS
Drupal
概要
Drupalは、2001年に誕生したオープンソースのエンタープライズ向けコンテンツ管理システムです。Symfonyフレームワークをベースに構築され、高度な拡張性とスケーラビリティを提供します。政府機関、大学、大企業など、セキュリティと柔軟性を重視する組織で広く採用されています。
詳細
Drupalは「ディストリビューション」と呼ばれる特定用途向けのパッケージを提供し、コマースサイト、政府サイト、教育機関向けサイトなど、様々な用途に最適化された構成で利用できます。
主な特徴:
- モジュラーアーキテクチャ: 45,000以上のモジュール(拡張機能)
- 多言語対応: 100以上の言語に対応、多言語サイトの構築が容易
- 強力なコンテンツモデリング: フィールドAPI、ビュー、コンテンツタイプ
- エンタープライズ機能: ワークフロー、リビジョン管理、権限管理
- API First: RESTful API、JSON:API標準サポート
- Symfonyベース: モダンなPHPフレームワークによる堅牢な基盤
- キャッシング: 高度なキャッシング機構による高速化
メリット・デメリット
メリット
- エンタープライズレベルのセキュリティ
- 高度にカスタマイズ可能なコンテンツ構造
- 強力な権限管理システム
- 優れたパフォーマンスとスケーラビリティ
- 活発な開発者コミュニティ
- 包括的なAPIサポート
- 多言語・多地域対応
- バージョン管理とワークフロー
デメリット
- 学習曲線が急峻
- 初期設定が複雑
- 小規模サイトには過剰な機能
- テーマ開発に専門知識が必要
- アップグレードが困難な場合がある
- ホスティング要件が高い
参考ページ
- Drupal公式サイト
- Drupal日本語コミュニティ
- Drupal API Documentation
- Drupal Module Repository
- Drupal Developer Guide
- Drupal 9/10 Documentation
書き方の例
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');
}
}
}