Drupal to jeden z najstarszych i nadal aktywnie rozwijanych systemów CMS w ekosystemie PHP. Wersja 8+ to kompletna przebudowa oparta na Symfony – jeśli znasz Magento 2, wiele konwencji będzie znajomych. Pokazuję jak myśleć o Drupalu z perspektywy PHP developera: architektura, system modułów, hooks i gdzie Drupal błyszczy względem WordPressa czy Magento.
Drupal 8+ – oparty na Symfony
Drupal 7 i starsze to własny, autorski framework. Drupal 8 (2015) to rewolucja – przepisanie na komponenty Symfony, wprowadzenie PSR-4 autoloadera, Composer, Twig jako silnik szablonów i OOP zamiast proceduralnego PHP. Drupal 10 (2022) usuwa starszy kod i wymaga PHP 8.1+.
Komponenty Symfony używane w Drupalu to m.in.: HttpFoundation, HttpKernel, Routing, EventDispatcher, DependencyInjection, Console, Validator. Znajome dla każdego kto pracuje z Magento 2.
Kiedy Drupal zamiast WordPressa lub Magento?
Drupal ma konkretną niszę – sprawdza się najlepiej gdy:
- Projekt wymaga zaawansowanego zarządzania treścią z wieloma typami zawartości i skomplikowanymi relacjami
- Potrzebujesz wielojęzycznej strony z pełnym workflow tłumaczeń
- Bezpieczeństwo i audyt dostępu to priorytet (rządy, banki, instytucje publiczne)
- Masz dużo heterogenicznych treści które nie pasują do prostego blog/sklep modelu
- Potrzebujesz elastycznego API-first CMS (headless Drupal przez JSON:API lub GraphQL)
Instalacja i podstawy
# Instalacja przez Composer composer create-project drupal/recommended-project my-drupal-site cd my-drupal-site # DDEV dla lokalnego środowiska ddev config --project-type=drupal10 --docroot=web ddev start ddev exec drush site:install --db-url=mysql://db:db@db/db -y # Drush - odpowiednik bin/magento w Drupalu ddev exec drush status ddev exec drush cache:rebuild # odpowiednik cache:flush w Magento ddev exec drush cron # uruchom cron ręcznie
System typów zawartości – odpowiednik produktów w Magento
W Drupalu dane są zorganizowane przez Entity Types i Bundles. Node (artykuł, strona) to Entity Type, a „Artykuł” i „Strona Podstawowa” to Bundles – każdy z własnym zestawem pól. To podobne do attribute sets w Magento, ale bardziej elastyczne:
<?php
declare(strict_types=1);
namespace Drupal\my_module\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* @ContentEntityType(
* id = "product",
* label = @Translation("Product"),
* base_table = "product",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid",
* },
* handlers = {
* "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\Core\Entity\EntityListBuilder",
* },
* )
*/
class Product extends ContentEntityBase
{
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array
{
$fields = parent::baseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setRequired(true)
->setSetting('max_length', 255)
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
]);
$fields['sku'] = BaseFieldDefinition::create('string')
->setLabel(t('SKU'))
->setRequired(true)
->addConstraint('UniqueField');
$fields['price'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Price'))
->setSetting('precision', 10)
->setSetting('scale', 2);
$fields['status'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Published'))
->setDefaultValue(true);
return $fields;
}
}
System hooków – rozszerzanie bez nadpisywania
Drupal ma dwa mechanizmy rozszerzania: klasyczne hooks (funkcje PHP o określonej nazwie) i nowoczesny EventDispatcher (Symfony). Hooks to dziedzictwo Drupala 7, ale nadal powszechnie używane:
<?php
// my_module.module - klasyczne hooks Drupala
// Nazwa funkcji = nazwa_modułu + nazwa_hooka
/**
* Implements hook_entity_presave().
* Wywoływany przed zapisem każdej encji - jak Observer w Magento.
*/
function my_module_entity_presave(\Drupal\Core\Entity\EntityInterface $entity): void
{
if ($entity->getEntityTypeId() !== 'node') {
return;
}
// Dodaj timestamp ostatniej modyfikacji do własnego pola
if ($entity->hasField('field_last_synced')) {
$entity->set('field_last_synced', \Drupal::time()->getCurrentTime());
}
}
/**
* Implements hook_form_alter().
* Modyfikacja formularzy - bardzo potężny hook.
*/
function my_module_form_alter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, string $form_id): void
{
if ($form_id !== 'node_article_form') {
return;
}
// Dodaj własną walidację do formularza artykułu
$form['#validate'][] = 'my_module_article_validate';
}
function my_module_article_validate(array &$form, \Drupal\Core\Form\FormStateInterface $form_state): void
{
$title = $form_state->getValue('title')[0]['value'] ?? '';
if (strlen($title) < 10) {
$form_state->setErrorByName('title', t('Tytuł musi mieć co najmniej 10 znaków.'));
}
}
Serwisy i Dependency Injection – jak w Symfony i Magento
# my_module/my_module.services.yml - jak di.xml w Magento
services:
my_module.product_service:
class: Drupal\my_module\Service\ProductService
arguments:
- '@entity_type.manager'
- '@logger.channel.my_module'
my_module.logger:
parent: logger.channel_base
arguments: ['my_module']
<?php
declare(strict_types=1);
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;
class ProductService
{
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
private LoggerInterface $logger
) {}
public function getActiveProducts(int $limit = 20): array
{
$storage = $this->entityTypeManager->getStorage('product');
$ids = $storage->getQuery()
->accessCheck(true)
->condition('status', 1)
->sort('name')
->range(0, $limit)
->execute();
if (empty($ids)) {
return [];
}
return $storage->loadMultiple($ids);
}
public function createProduct(array $values): \Drupal\my_module\Entity\Product
{
$storage = $this->entityTypeManager->getStorage('product');
$product = $storage->create($values);
$product->save();
$this->logger->info('Product created: {sku}', ['sku' => $values['sku']]);
return $product;
}
}
Headless Drupal – JSON:API i GraphQL
Drupal 8.7+ ma wbudowany moduł JSON:API który automatycznie wystawia REST API dla wszystkich typów encji. Zero konfiguracji kodu – włącz moduł i masz API:
# Włącz JSON:API
ddev exec drush pm:enable jsonapi -y
# Teraz masz automatycznie wygenerowane endpointy:
# GET /jsonapi/node/article - lista artykułów
# GET /jsonapi/node/article/{uuid} - pojedynczy artykuł
# POST /jsonapi/node/article - utwórz artykuł (wymaga auth)
# PATCH /jsonapi/node/article/{uuid} - aktualizuj artykuł
# DELETE /jsonapi/node/article/{uuid} - usuń artykuł
# Filtrowanie przez URL
# GET /jsonapi/node/article?filter[status]=1&sort=-created&page[limit]=10
// Konsumpcja Drupal JSON:API z Vue/React/Next.js
const response = await fetch(
'/jsonapi/node/article?include=field_image,field_tags&page[limit]=10',
{
headers: {
'Accept': 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json',
},
}
);
const data = await response.json();
const articles = data.data.map(item => ({
id: item.id,
title: item.attributes.title,
body: item.attributes.body?.value,
image: item.relationships.field_image?.data?.id,
}));
Porównanie Drupal vs WordPress vs Magento
| Aspekt | Drupal 10 | WordPress | Magento 2 |
|---|---|---|---|
| Główne zastosowanie | Enterprise CMS / API | Blog / prosty CMS | E-commerce |
| Framework bazowy | Symfony | Własny | Symfony + Laminas |
| Krzywa uczenia | Stroma | Łagodna | Bardzo stroma |
| Typowanie PHP | Dobre | Słabe | Bardzo dobre |
| API-first / Headless | Wbudowane JSON:API | WP REST API (ograniczone) | REST + GraphQL |
| Wielojęzyczność | Doskonała | Przez wtyczki | Dobra (store views) |
Podsumowanie
Drupal 10 to dojrzała platforma dla projektów które wymagają elastycznego zarządzania treścią na dużą skalę. Oparcie na Symfony sprawia że PHP developer z doświadczeniem Magento lub Symfony poczuje się zadomowiony szybciej niż w WordPressie. System hooków jest starszy i bardziej proceduralny niż eventy Magento, ale EventDispatcher jest też dostępny. Headless Drupal przez JSON:API to jeden z najczystszych gotowych rozwiązań API-first CMS w ekosystemie PHP.
