Drupal

High-performance CMS for enterprises. Provides flexibility and scalability to handle complex requirements.

CMSOpen SourceEnterprisePHPSymfony
License
GPL v2
Language
PHP
Pricing
完全無料(自己ホスト)
Official Site
Visit Official Site

CMS

Drupal

Overview

Drupal is an open-source enterprise content management system that was born in 2001. Built on the Symfony framework, it provides advanced extensibility and scalability. It is widely adopted by organizations that prioritize security and flexibility, including government agencies, universities, and large corporations.

Details

Drupal offers "distributions" - packages optimized for specific use cases such as commerce sites, government sites, and educational institution websites, allowing usage with configurations tailored to various purposes.

Key Features:

  • Modular Architecture: Over 45,000 modules (extensions)
  • Multilingual Support: Supports 100+ languages, easy to build multilingual sites
  • Powerful Content Modeling: Fields API, Views, Content Types
  • Enterprise Features: Workflow, revision management, access control
  • API First: RESTful API, JSON:API standard support
  • Symfony Based: Robust foundation with modern PHP framework
  • Caching: High-speed performance through advanced caching mechanisms

Pros and Cons

Pros

  • Enterprise-level security
  • Highly customizable content structure
  • Powerful access control system
  • Excellent performance and scalability
  • Active developer community
  • Comprehensive API support
  • Multilingual and multi-regional support
  • Version control and workflow

Cons

  • Steep learning curve
  • Complex initial setup
  • Overkill for small sites
  • Requires expertise for theme development
  • Upgrades can be difficult
  • High hosting requirements

References

Code Examples

1. Hello World (Basic Setup)

Basic Module Structure (hello_world.info.yml)

name: 'Hello World'
type: module
description: 'A hello world module example'
package: 'Custom'
core_version_requirement: ^10
dependencies:
  - drupal:system (>=10.0)

Basic Controller (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 Controller
 */
class HelloController extends ControllerBase {

  /**
   * Logger factory
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * Constructor
   */
  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 page
   *
   * @return array
   *   Render 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. Theme Development

Theme Info File (mytheme.info.yml)

name: 'My Custom Theme'
type: theme
description: 'A custom Drupal theme'
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'

Page Template (templates/page.html.twig)

{#
/**
 * @file
 * Default page template
 */
#}
<!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>

Region Template (templates/region.html.twig)

{#
/**
 * @file
 * Default region template
 */
#}
{% if content %}
  <div{{ attributes.addClass('region', 'region-' ~ region|clean_class) }}>
    {{ content }}
  </div>
{% endif %}

3. Module Development

Custom Block Module (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;

/**
 * Provides a custom block
 *
 * @Block(
 *   id = "my_module_custom_block",
 *   admin_label = @Translation("Custom Block"),
 *   category = @Translation("Custom")
 * )
 */
class CustomBlock extends BlockBase implements BlockPluginInterface, ContainerFactoryPluginInterface {

  /**
   * Entity type manager
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructor
   */
  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();
    
    // Get recent content
    $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');
  }

  /**
   * Get content type options
   */
  protected function getContentTypeOptions() {
    $options = [];
    $types = $this->entityTypeManager
      ->getStorage('node_type')
      ->loadMultiple();
    
    foreach ($types as $type) {
      $options[$type->id()] = $type->label();
    }
    
    return $options;
  }
}

4. Custom Post Types (Content Types)

Custom Entity Definition (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;

/**
 * Defines the Product entity
 *
 * @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. Database Operations

Database Queries and Transactions (DatabaseExample.php)

<?php

namespace Drupal\my_module\Service;

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

/**
 * Database operations example
 */
class DatabaseExample {

  use StringTranslationTrait;

  /**
   * Database connection
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Logger service
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Constructor
   */
  public function __construct(Connection $database, LoggerChannelFactoryInterface $logger_factory) {
    $this->database = $database;
    $this->logger = $logger_factory->get('my_module');
  }

  /**
   * Insert data
   */
  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;
    }
  }

  /**
   * Retrieve data
   */
  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();
  }

  /**
   * Get data by condition
   */
  public function getDataByName($name) {
    $query = $this->database->select('my_module_data', 'md')
      ->fields('md')
      ->condition('name', $name, '=');
    
    return $query->execute()->fetchObject();
  }

  /**
   * Update data
   */
  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;
    }
  }

  /**
   * Delete data
   */
  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;
    }
  }

  /**
   * Transaction processing
   */
  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();
      }
      
      // Update statistics
      $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;
    }
  }

  /**
   * Complex query example
   */
  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();
  }

  /**
   * Join query example
   */
  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 Integration

REST API Resource Plugin (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;

/**
 * Provides a REST resource for products
 *
 * @RestResource(
 *   id = "product_resource",
 *   label = @Translation("Product Resource"),
 *   uri_paths = {
 *     "canonical" = "/api/product/{id}",
 *     "collection" = "/api/products"
 *   }
 * )
 */
class ProductResource extends ResourceBase {

  /**
   * Entity type manager
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructor
   */
  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')
    );
  }

  /**
   * Responds to GET requests
   */
  public function get($id = NULL) {
    if ($id) {
      // Get single product
      $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 {
      // Get product list
      $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);
    }
  }

  /**
   * Responds to POST requests
   */
  public function post($data) {
    // Check permissions
    if (!\Drupal::currentUser()->hasPermission('create product entities')) {
      throw new AccessDeniedHttpException();
    }
    
    // Validation
    if (empty($data['name']) || empty($data['price'])) {
      throw new BadRequestHttpException('Name and price are required');
    }
    
    // Create product entity
    $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');
    }
  }

  /**
   * Responds to PATCH requests
   */
  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();
    }
    
    // Update only allowed fields
    $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');
    }
  }

  /**
   * Responds to DELETE requests
   */
  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');
    }
  }
}