Drupal 10 is a significantly more modern platform than its reputation suggests. The rewrite to Symfony components, a proper DI container, and a composer-first approach happened years ago. Drupal 10 adds PHP 8.1+ requirements, drops old dependencies, and has CKEditor 5 and Olivero as defaults. I show Drupal 10 through the lens of a PHP developer familiar with Magento 2 and Symfony.
Architecture similarities with Magento 2
| Concept | Drupal 10 | Magento 2 |
|---|---|---|
| DI Container | Symfony DI | Custom DI (similar) |
| Modules | Drupal modules | Magento modules |
| Events | Symfony EventDispatcher + Hooks | Magento EventManager |
| Routing | Symfony Routing + routing.yml | Custom routes.xml |
| ORM | Entity API (custom) | EAV + Collection |
| Templating | Twig | PHTML + KnockoutJS |
| Config | YAML files | XML files |
Entity Types – Drupal’s equivalent of Magento models
<?php
declare(strict_types=1);
// Custom entity type - like Magento's own Model/ResourceModel combo
// but with UI automatically generated by Drupal
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 = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\my_module\ProductListBuilder",
* "views_data" = "Drupal\views\EntityViewsData",
* "form" = {
* "add" = "Drupal\my_module\Form\ProductForm",
* "edit" = "Drupal\my_module\Form\ProductForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* },
* },
* links = {
* "canonical" = "/product/{product}",
* "add-form" = "/product/add",
* "edit-form" = "/product/{product}/edit",
* "delete-form" = "/product/{product}/delete",
* "collection" = "/admin/products",
* },
* )
*/
class Product extends ContentEntityBase
{
public static function baseFieldDefinitions(EntityTypeInterface $entityType): array
{
$fields = parent::baseFieldDefinitions($entityType);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setRequired(true)
->setSetting('max_length', 255)
->setDisplayConfigurable('form', true)
->setDisplayConfigurable('view', true);
$fields['price'] = BaseFieldDefinition::create('decimal')
->setLabel(t('Price'))
->setRequired(true)
->setSettings(['precision' => 10, 'scale' => 2])
->setDisplayConfigurable('form', true);
$fields['sku'] = BaseFieldDefinition::create('string')
->setLabel(t('SKU'))
->setRequired(true)
->addConstraint('UniqueField')
->setDisplayConfigurable('form', true);
return $fields;
}
}
Hooks – Drupal’s event system
<?php
// Drupal hooks - procedural functions in .module files
// Equivalent to Magento's event observers (but older style)
/**
* Implements hook_node_insert() - fires when a new node is created
*/
function my_module_node_insert(\Drupal\node\NodeInterface $node): void
{
if ($node->getType() === 'article') {
\Drupal::service('my_module.article_indexer')->index($node);
}
}
/**
* Implements hook_form_alter() - modify any form
* Equivalent to Magento's around plugin on form rendering
*/
function my_module_form_alter(array &$form, \Drupal\Core\Form\FormStateInterface $form_state, string $form_id): void
{
if ($form_id === 'node_article_form') {
$form['field_seo_title']['#description'] = t('Keep under 60 characters');
$form['#validate'][] = 'my_module_article_validate';
}
}
// Symfony EventSubscriber - modern alternative to hooks (Drupal 8+)
class ArticleEventSubscriber implements \Symfony\Component\EventDispatcher\EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
\Drupal\Core\Entity\EntityEvents::INSERT => 'onEntityInsert',
];
}
public function onEntityInsert(\Drupal\Core\Entity\EntityInterface $entity): void
{
if ($entity instanceof \Drupal\node\NodeInterface && $entity->getType() === 'article') {
// handle new article
}
}
}
Headless Drupal through JSON:API
# JSON:API is included in Drupal 10 core - zero configuration
# Enable the module:
drush en jsonapi -y
# Query content via REST
curl https://drupal10.example.com/jsonapi/node/article \
-H 'Accept: application/vnd.api+json'
# With filters
curl "https://drupal10.example.com/jsonapi/node/article?filter[status]=1&page[limit]=10"
# With relationships (includes)
curl "https://drupal10.example.com/jsonapi/node/article?include=field_author,field_category"
<?php
// Consuming Drupal JSON:API from a Magento module (e.g. CMS content sync)
class DrupalContentClient
{
public function __construct(
private \Magento\Framework\HTTP\Client\Curl $curl,
private string $drupalBaseUrl
) {}
public function getArticles(int $limit = 10): array
{
$url = $this->drupalBaseUrl . '/jsonapi/node/article'
. '?filter[status]=1'
. '&page[limit]=' . $limit
. '&include=field_category';
$this->curl->get($url);
$response = json_decode($this->curl->getBody(), true, 512, JSON_THROW_ON_ERROR);
return array_map(fn($item) => [
'id' => $item['id'],
'title' => $item['attributes']['title'],
'body' => $item['attributes']['body']['value'] ?? '',
'slug' => $item['attributes']['path']['alias'] ?? '',
], $response['data'] ?? []);
}
}
Summary
Drupal 10 is a respectable modern PHP platform. For a Magento 2 developer the DI container and Symfony-based architecture feel familiar, though the entity system and hooks require learning new conventions. Drupal’s real strength is structured content management with complex editorial workflows – things Magento does not do well. The JSON:API module makes headless integration clean and standard. Worth knowing when a client needs content-heavy sites alongside or instead of e-commerce.
