Vývoj v Drupalu
Drupal Development Guide
This comprehensive guide covers Drupal CMS development, including installation, module development, theming, security, and best practices for building robust Drupal applications.
Drupal Overview
Drupal is a free, open-source content management system (CMS) written in PHP. It’s known for its flexibility, scalability, and extensive customization capabilities through modules and themes.
Key Features
- Modular Architecture: Extensive module ecosystem
- Flexible Content Types: Custom content structures
- Powerful Taxonomy: Hierarchical classification system
- User Management: Role-based permissions
- Multilingual Support: Built-in internationalization
- API-First: Headless capabilities with JSON:API
- Security: Strong security track record
Installation and Setup
System Requirements
- PHP: 8.1 or higher
- Database: MySQL 5.7.8+ / MariaDB 10.3.7+ / PostgreSQL 12+
- Web Server: Apache/Nginx with mod_rewrite
- Memory: Minimum 256MB, recommended 512MB+
Installation Methods
Composer Installation (Recommended)
# Install Drupal using Composer
composer create-project drupal/recommended-project my-drupal-site
cd my-drupal-site
# Install dependencies
composer install
# Set up web server to point to web/ directory
Drush Installation
# Install Drush globally
composer global require drush/drush
# Download Drupal core
drush dl drupal
# Install Drupal
drush site-install standard \
--db-url='mysql://username:password@localhost/databasename' \
--site-name='My Drupal Site' \
--account-name=admin \
--account-pass=admin
Directory Structure
my-drupal-site/
├── composer.json
├── vendor/ # Composer dependencies
├── web/ # Drupal core and sites
│ ├── core/ # Drupal core
│ ├── modules/ # Contributed modules
│ │ ├── contrib/ # Downloaded modules
│ │ └── custom/ # Custom modules
│ ├── themes/ # Themes
│ │ ├── contrib/ # Downloaded themes
│ │ └── custom/ # Custom themes
│ ├── sites/ # Multisite configuration
│ └── index.php
├── config/ # Configuration export
└── drush/ # Drush configuration
Module Development
Creating a Custom Module
Module Structure:
modules/custom/my_module/
├── my_module.info.yml # Module metadata
├── my_module.module # Main module file
├── my_module.routing.yml # Route definitions
├── src/ # PHP classes
│ ├── Controller/
│ ├── Plugin/
│ ├── Form/
│ └── Service/
├── templates/ # Twig templates
├── css/ # Stylesheets
├── js/ # JavaScript files
└── config/ # Configuration schemas
Module Info File (my_module.info.yml):
name: 'My Custom Module'
description: 'Provides custom functionality for my site.'
package: 'Custom'
type: module
core_version_requirement: ^9 || ^10
dependencies:
- drupal:node
- drupal:user
configure: my_module.settings
Basic Module File (my_module.module):
<?php
use Drupal\Core\Routing\RouteMatchInterface;
/**
* @file
* Contains my_module.module.
*/
/**
* Implements hook_help().
*/
function my_module_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.my_module':
return '<p>' . t('Help text for My Module.') . '</p>';
}
}
/**
* Implements hook_theme().
*/
function my_module_theme() {
return [
'my_custom_template' => [
'variables' => [
'title' => NULL,
'content' => NULL,
],
],
];
}
Controllers
Basic Controller:
<?php
namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
* Returns responses for My Module routes.
*/
class MyModuleController extends ControllerBase {
/**
* Builds the response.
*/
public function build() {
$build['content'] = [
'#type' => 'item',
'#markup' => $this->t('Hello, Drupal!'),
];
return $build;
}
}
Routing
Routing File (my_module.routing.yml):
my_module.content:
path: '/my-module'
defaults:
_controller: '\Drupal\my_module\Controller\MyModuleController::build'
_title: 'My Module'
requirements:
_permission: 'access content'
Forms
Custom Form:
<?php
namespace Drupal\my_module\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Class MyModuleForm.
*/
class MyModuleForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'my_module_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#required' => TRUE,
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->messenger()->addMessage($this->t('Form submitted successfully!'));
}
}
Services
Service Definition (my_module.services.yml):
services:
my_module.custom_service:
class: Drupal\my_module\Service\CustomService
arguments: ['@entity_type.manager', '@current_user']
Service Class:
<?php
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
/**
* Class CustomService.
*/
class CustomService {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs a new CustomService object.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $current_user) {
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
}
/**
* Custom business logic.
*/
public function doSomething() {
// Implementation here
}
}
Theming
Theme Structure
themes/custom/my_theme/
├── my_theme.info.yml # Theme metadata
├── my_theme.libraries.yml # Asset libraries
├── my_theme.breakpoints.yml # Responsive breakpoints
├── css/ # Stylesheets
├── js/ # JavaScript files
├── images/ # Theme images
├── templates/ # Twig templates
├── includes/ # PHP includes
└── config/ # Theme configuration
Theme Info File (my_theme.info.yml):
name: 'My Custom Theme'
description: 'A custom theme for my Drupal site.'
type: theme
base theme: stable9
core_version_requirement: ^9 || ^10
libraries:
- my_theme/global
regions:
header: 'Header'
content: 'Content'
sidebar: 'Sidebar'
footer: 'Footer'
Twig Templates
Custom Node Template (node–article–full.html.twig):
{#
/**
* @file
* Theme override for a node in full view mode.
*/
#}
<article{{ attributes.addClass('node--type-' ~ node.bundle|clean_class, 'node--view-mode-' ~ view_mode|clean_class) }}>
{{ title_prefix }}
<h1{{ title_attributes }}>
<a href="{{ url }}" rel="bookmark">{{ label }}</a>
</h1>
{{ title_suffix }}
{% if display_submitted %}
<div class="node__meta">
{{ author_picture }}
<span{{ author_attributes }}>
{% trans %}Submitted by {{ author_name }} on {{ date }}{% endtrans %}
</span>
{{ metadata }}
</div>
{% endif %}
<div{{ content_attributes.addClass('node__content') }}>
{{ content }}
</div>
</article>
Asset Libraries
Libraries File (my_theme.libraries.yml):
global:
version: VERSION
css:
theme:
css/style.css: {}
js:
js/script.js: {}
dependencies:
- core/jquery
- core/drupal
custom-page:
css:
theme:
css/custom-page.css: {}
Preprocessing Functions
template.php or .theme file:
<?php
/**
* @file
* Functions to support theming in the My Theme theme.
*/
/**
* Implements hook_preprocess_node().
*/
function my_theme_preprocess_node(&$variables) {
$node = $variables['node'];
// Add custom classes based on content type
$variables['attributes']['class'][] = 'node--type-' . $node->bundle();
// Add custom variables
$variables['custom_field'] = $node->get('field_custom')->value;
}
/**
* Implements hook_preprocess_page().
*/
function my_theme_preprocess_page(&$variables) {
// Add body classes
$variables['attributes']['class'][] = 'page-' . \Drupal::routeMatch()->getRouteName();
}
Content Types and Fields
Creating Content Types Programmatically
Hook Implementation:
<?php
use Drupal\node\Entity\NodeType;
/**
* Implements hook_install().
*/
function my_module_install() {
// Create content type
$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
'description' => 'Use articles for time-sensitive content like news, press releases or blog posts.',
]);
$node_type->save();
// Add body field
$field_storage = \Drupal::entityTypeManager()
->getStorage('field_storage_config')
->create([
'field_name' => 'body',
'entity_type' => 'node',
'type' => 'text_with_summary',
]);
$field_storage->save();
$field = \Drupal::entityTypeManager()
->getStorage('field_config')
->create([
'field_name' => 'body',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Body',
]);
$field->save();
}
Custom Fields
Field Type Creation:
<?php
namespace Drupal\my_module\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* @FieldType(
* id = "my_custom_field",
* label = @Translation("My Custom Field"),
* description = @Translation("A custom field type."),
* category = @Translation("Custom"),
* default_widget = "my_custom_widget",
* default_formatter = "my_custom_formatter"
* )
*/
class MyCustomField extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Value'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'varchar',
'length' => 255,
],
],
];
}
}
Drupal Security
Writing Secure Code
Input Sanitization:
// JavaScript sanitization
var rawInputText = $('#form-input').text();
var escapedInputText = Drupal.checkPlain(rawInputText);
// PHP sanitization
$safe_output = \Drupal\Component\Utility\Html::escape($user_input);
$admin_safe = \Drupal\Component\Utility\Xss::filterAdmin($input);
SQL Injection Prevention:
// Using query with placeholders
$result = \Drupal::database()->query(
'SELECT foo FROM {table} t WHERE t.name = :name',
[':name' => $_GET['user']]
);
// Using select API
$users = ['joe', 'poe', $_GET['user']];
$result = \Drupal::database()->select('foo', 'f')
->fields('f', ['bar'])
->condition('f.bar', $users)
->execute();
// LIKE queries with escaping
$conn = \Drupal::database();
$result = $conn->select('table', 't')
->condition('t.field', '%' . $conn->escapeLike($user), 'LIKE')
->execute();
Access Control
Permissions:
/**
* Implements hook_permission().
*/
function my_module_permission() {
return [
'access my_module content' => [
'title' => t('Access My Module content'),
'description' => t('Allow access to My Module content.'),
],
];
}
Node Access:
/**
* Implements hook_node_access().
*/
function my_module_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account) {
if ($node->bundle() === 'restricted_content' && $op === 'view') {
return \Drupal\Core\Access\AccessResult::forbiddenIf(!$account->hasPermission('view restricted content'));
}
return \Drupal\Core\Access\AccessResult::neutral();
}
CSRF Protection
Form CSRF Protection:
$form['#token'] = 'my_custom_form';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => t('Submit'),
];
Route CSRF Protection:
my_module.custom_route:
path: '/my-custom-path'
defaults:
_controller: '\Drupal\my_module\Controller\MyController::customAction'
requirements:
_csrf_token: 'TRUE'
XSS Prevention
Twig Auto-escaping:
{# Safe - auto-escaped #}
{{ user_input }}
{# Explicit escaping #}
{{ user_input|escape }}
{# Raw output (use with caution) #}
{{ user_input|raw }}
PHP XSS Protection:
// Use Drupal's sanitization functions
$safe_markup = \Drupal\Component\Utility\Xss::filter($user_input);
$admin_markup = \Drupal\Component\Utility\Xss::filterAdmin($user_input);
Drush Commands
Common Drush Commands
Site Management:
# Clear all caches
drush cr
# Rebuild permissions
drush php-eval 'node_access_rebuild();'
# Update database
drush updb
# Import/export configuration
drush config-export
drush config-import
Content Management:
# Create user
drush user-create username --mail="user@example.com" --password="password"
# Generate content
drush generate-content 10 node --types=article
# List users
drush user-list
Module Management:
# Enable module
drush en my_module
# Disable module
drush pmu my_module
# Uninstall module
drush pm-uninstall my_module
Performance Optimization
Caching
Cache Tags:
$build['#cache'] = [
'tags' => ['node:' . $node->id()],
'contexts' => ['user'],
'max-age' => 3600,
];
Custom Cache:
$cid = 'my_module:data:' . $user->id();
$data = \Drupal::cache()->get($cid);
if (!$data) {
$data = expensive_operation();
\Drupal::cache()->set($cid, $data, time() + 3600);
}
Database Optimization
Efficient Queries:
// Use entity queries for better performance
$query = \Drupal::entityQuery('node')
->condition('type', 'article')
->condition('status', 1)
->range(0, 10);
$nids = $query->execute();
$nodes = \Drupal::entityTypeManager()
->getStorage('node')
->loadMultiple($nids);
Image Optimization
Responsive Images:
$responsive_image = [
'#theme' => 'responsive_image',
'#responsive_image_style_id' => 'my_responsive_style',
'#uri' => $file_uri,
];
Testing
Unit Testing
Test Class:
<?php
namespace Drupal\Tests\my_module\Unit;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\my_module\Service\CustomService
* @group my_module
*/
class CustomServiceTest extends UnitTestCase {
/**
* @covers ::doSomething
*/
public function testDoSomething() {
$service = new CustomService();
$result = $service->doSomething();
$this->assertEquals('expected', $result);
}
}
Functional Testing
Functional Test:
<?php
namespace Drupal\Tests\my_module\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* @group my_module
*/
class MyModuleTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['my_module'];
/**
* Test custom functionality.
*/
public function testCustomFunctionality() {
$this->drupalGet('/my-module');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Hello, Drupal!');
}
}
Deployment
Configuration Management
Export Configuration:
drush config-export --destination=/path/to/config
Import Configuration:
drush config-import --source=/path/to/config
Environment-Specific Settings
settings.php:
<?php
// Database configuration
$databases['default']['default'] = [
'database' => getenv('DB_NAME'),
'username' => getenv('DB_USER'),
'password' => getenv('DB_PASS'),
'host' => getenv('DB_HOST'),
'driver' => 'mysql',
];
// Trusted host patterns
$settings['trusted_host_patterns'] = [
'^example\.com$',
'^.+\.example\.com$',
];
// Environment indicator
$config['system.site']['name'] = getenv('SITE_NAME') ?: 'My Drupal Site';
Best Practices
Code Standards
- Follow Drupal Coding Standards: Use Drupal Code Sniffer
- Use Dependency Injection: Avoid static calls when possible
- Implement Proper Access Control: Check permissions appropriately
- Sanitize All Input: Never trust user input
- Use Render Arrays: Build output declaratively
- Cache Aggressively: Implement appropriate caching strategies
Module Development
- Keep Modules Focused: Single responsibility principle
- Use Hooks Appropriately: Prefer events for complex logic
- Document Your Code: Comprehensive inline documentation
- Test Thoroughly: Unit and functional tests
- Version Properly: Semantic versioning
- Provide Updates: Safe configuration updates
Performance
- Optimize Database Queries: Use indexes and efficient queries
- Implement Caching: Cache rendered output and expensive operations
- Use CDNs: Serve static assets from CDNs
- Minimize HTTP Requests: Combine and compress assets
- Monitor Performance: Use tools like New Relic or Blackfire
Security
- Regular Updates: Keep Drupal core and modules updated
- Strong Passwords: Enforce password policies
- Two-Factor Authentication: Enable 2FA for admin accounts
- Regular Backups: Automated backup strategies
- Security Modules: Use contributed security modules
- Audit Logs: Monitor and log security events
This guide provides a comprehensive foundation for Drupal development, covering everything from basic installation to advanced module development, theming, and security practices.