PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Drupal 10 – Entity Types, hooks, DI, headless przez JSON:API, porównanie z WP i Magento

by Henryk Tews / wtorek, 11 kwietnia 2023 / Opublikowano w PHP

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.

About Henryk Tews

Co możesz przeczytać następne

PHP 8.4 RC – property hooks w praktyce, niespodzianki, asymmetric visibility
PHP 8.2 preview – readonly classes, DNF types, deprecacja dynamicznych właściwości
Dijkstra – najkrótsza ścieżka z wagami, SplMinHeap, zastosowania w e-commerce
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}