Backwards Compatibility

Wim Leers

Principal Software Engineer, Drupal Acceleration Team ("OCTO"), Acquia logo

Things I learned so I know slightly better what I do not know πŸ€“

Drupal & d.o infrastructure: from πŸ”Ž to πŸ₯³

can be used for anything!


All code must have an API to be generic & overridable
but not enough time to carefully design every API


… yet underengineered!


BC = nightmare. Let's get better.


We have gotten better! πŸ₯³

Backwards compatibility

Promise of updating without things breaking


Does this mean I can't ever fix mistakes from the past?

Does this mean I can never do this cool thing?


I can't possibly maintain this spaghetti code of backwards compatibility layers!

d.o/core/d8-bc-policy: @api vs @internal


But in Drupal core…

0 occurrences
53 occurrences

99% "undocumented"
β‡’ 99% considered API


Two years later:

  1. More code considered "internal APIs" and 53 711 @internal occurrences
  2. For public APIs, the deprecation process is a is a requirement; for internal APIs, we provide BC and deprecation where possible, but might make breaking changes in situations where BC is not possible or has negative consequences.





Two years later:

Current best practice?

API providers

  1. Don't break existing callers.
  2. Notify existing callers of changed code.
 * Sets a message to display to the user.
 * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
 *   Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead.
function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) {
  @trigger_error('drupal_set_message() is deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Messenger\MessengerInterface::addMessage() instead. See', E_USER_DEPRECATED);
  $messenger = \Drupal::messenger();
  if (isset($message)) {
    $messenger->addMessage($message, $type, $repeat);
  return $messenger->all();

API consumers

  1. Get notified of deprecations in automated testing:
            types: 'PHPUnit-Unit'
            testgroups: '--all'
            suppress-deprecations: false # πŸ₯³
            halt-on-fail: false
  2. Update existing callers retaining BC:
        if (\Drupal::hasService('messenger')) {
        else {
Deprecation causing test failure.

#3024461: Consistent deprecation messages format




Q: Why does Drupal have so many APIs?
A: Optimized for targeted overrides
    β‡’ granular APIs
    β‡’ many, many APIs

  • forest ~ Drupal
  • tree ~ Drupal component
  • branch ~ Drupal component feature
  • leaf ~ Drupal component feature method

Drupal allows you to replace a particular leaf.
Others require replacing a branch or even a tree.

Drupal has 3 types of APIs

Explicit APIs
hooks, plugin types, tagged services
Implicit APIs
markup structure, render array structure, call order (weights, priorities)
Accidental APIs
many (most?) interfaces (and even classes!)

The API assumption

  • Drupal: X is API
  • Others: X is NOT an API


Part 1: Accidental API

  1. D8: OOPify everything
  2. Every class must have an interface
  3. Interfaces coupled to the sole implementation
  4. BC broken by bugfixes & new implementations


Issue #2266809: Make QuickEditEntityFieldAccessCheck::access() use the $account that's passed in

    * @param string $field_name
    *   The field name.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user for which to check access.
    * @return \Drupal\Core\Access\AccessResultInterface
    *   The access result.
-  public fn accessEditEntityField($entity, $field_name);
+  public fn accessEditEntityField($entity, $field_name, AccountInterface $account);


(Introduced by yours truly in #1824500: In-place editing for Fields on Dec 21, 2012.
Suffering the consequences many years later.)

Poorly designed APIs make BC very difficult

API support cost

Prefer duplication over the wrong abstraction

API when:

  1. data to prove soundness of API design
  2. sufficient demand

API discoverability & complexity

Little work + high complexity (granular APIs)


More work + low complexity (duplication)


Part 2: orthogonality

[…] how a relatively small number of components can be combined in a relatively small number of ways to get the desired results. It is associated with simplicity; the more orthogonal the design, the fewer exceptions. This makes it easier to learn […]

API dependencies

  • Automated Cron: 5 (Form + Config + EventSub + Cron + State)
  • BigPipe: 5 (Render + AJAX + Cache + EventSub + Req/Resp)
  • Entity API uses >10 APIs
β‡’ using Entity API === using >10 APIs!

API cascades

NodeInterface $node

1. NodeInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, RevisionLogInterface, EntityPublishedInterface
2. ContentEntityInterface extends \Traversable, FieldableEntityInterface, RevisionableInterface, TranslatableInterface
3. FieldableEntityInterface extends EntityInterface
4. EntityInterface extends AccessibleInterface, CacheableDependencyInterface, RefinableCacheableDependencyInterface

FieldableEntityInterface::get() aka $node->get($field_name)

1. FieldItemListInterface extends ListInterface, AccessibleInterface
2. ListInterface extends TraversableTypedDataInterface, \ArrayAccess, \Countable
3. TraversableTypedDataInterface extends TypedDataInterface, \Traversable
4. TypedDataInterface
Massive composition & long inheritance chains require massive knowledge


Part 3: assumptions

  1. Drupal core does X
  2. Module/API Foo assumes X
  3. Install contrib module Bar: X β‡’ Y
  4. 😭
Zero config, no UI!

Breaks when advagg is installed 😭

     $request = $this->requestStack->getCurrentRequest();
     $link_headers = $request->attributes->get('http2_server_push_link_headers', []);
     foreach ($elements as &$element) {
+      if (!static::isLinkRelStylesheet($element)) {
+        continue;
+      }
       // Locally served CSS files that are sent to all browsers can be pushed.
-      if ($element['#tag'] === 'link' && $element['#browsers']['!IE'] === TRUE && $element['#browsers']['IE'] === TRUE && $element['#attributes']['href'][0] === '/' && $element['#attributes']['href'][1] !== '/') {
+      if (isset($element['#attributes']['href']) && static::hasRootRelativeUrl($element, 'href') && static::isUnconditional($element)) {
         $link_header_value = '<' . $element['#attributes']['href'] . '>; rel=preload; as=style';
         $link_headers[] = $link_header_value;
Make assumptions explicit

even better:

Test assumptions

Functionality-only modules

BigPipe + Dynamic Page Cache

5 issues in 2017, 13 in total by 2019.
For millions of responses accelerated!
Try hard to not provide an API

(Then BC is kept as long as functionality works!)

Functionality first, API later
or mark @internal first, remove later
Prefer duplication over the wrong abstraction
Test 1) critical path, 2) edge cases, 3) assumptions
(especially for APIs)



Locate this session at the
DrupalCon Seattle website:

Take the survey!