Skip to content

Best Practices: Performance & Security

When to Use

You want to optimize custom field performance and ensure secure data handling.

Performance

Single-table advantage:

  • Custom Field stores all sub-fields in one table row -- single query to load all data
  • Paragraphs creates N entities -- N+1 query problem (1 parent + N children)
  • For 10 sub-fields: Custom Field = 1 JOIN, Paragraphs = 10+ JOINs

Optimization patterns:

// GOOD: Load entity once, access multiple sub-fields
$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
$street = $node->field_address->street;
$city = $node->field_address->city;
$state = $node->field_address->state;

// BAD: Loading entity repeatedly
$street = Node::load($nid)->field_address->street;
$city = Node::load($nid)->field_address->city; // Redundant load

Views performance:

  • All columns in same table -- no relationship needed
  • Filter/sort on custom field columns = single table query
  • vs Paragraphs: require relationships, multiple JOIN tables

Field count limits:

  • MySQL row size limit: ~8KB per row
  • Practical limit: ~50-100 sub-fields depending on types
  • For 100+ fields: consider custom field type plugin instead

Caching:

  • Field values cached with entity
  • No special caching needed
  • Clear entity cache when updating programmatically

Security

Input validation:

  • Widget validation runs automatically
  • For custom widgets, implement validation in widget plugin
  • Don't trust programmatic imports -- validate types

XSS prevention:

// GOOD: Use render arrays with automatic escaping
return [
  '#markup' => $this->t('Value: @value', ['@value' => $value]),
];

// BAD: Raw concatenation
return '<div>' . $value . '</div>'; // XSS vulnerability

Entity reference access checks:

// GOOD: Check access before rendering
$entity = $storage->load($entity_id);
if ($entity && $entity->access('view')) {
  return $entity->toLink()->toRenderable();
}

// BAD: Assume access
return $storage->load($entity_id)->toLink(); // May expose restricted content

File upload security:

  • Always validate file extensions in widget settings
  • Use private:// scheme for sensitive files
  • Configure upload_location to segregate files
  • Set max_filesize to prevent DoS

Link security:

  • Use noopener noreferrer for target="_blank"
  • Validate external URLs with allowed protocols
  • Built-in constraints: LinkExternalProtocolsConstraint, LinkAccessConstraint

SQL injection prevention:

  • Custom Field uses Entity API -- parameterized queries automatic
  • For custom queries, use query builders with placeholders
  • Never concatenate user input into SQL

Common Mistakes

  • Loading entities in loops -- Load once, access multiple times; or use loadMultiple()
  • Not sanitizing output in custom formatters -- Use render arrays with #markup and placeholders, not raw concatenation
  • Exposing entity references without access checks -- Always check $entity->access('view')
  • Public files for private data -- Use private:// scheme and configure file access controls
  • Complex joins in Views -- Custom Field columns in same table; avoid relationship if possible

See Also

  • OWASP: https://owasp.org/www-project-top-ten/
  • Drupal security: https://www.drupal.org/security/secure-coding-practices
  • Development Standards