Drupal deprecation policy

Last updated on
30 January 2024

Introduction

We deprecate code to keep the backwards compatibility promise and to provide a continuous upgrade path.

This document explains how to deprecate code and what can be deprecated.

Test classes are not deprecated. Any test that could be considered deprecated should be removed or modified to be relevant.

Testing

Tests which exercise deprecated code can be annotated with @group legacy in order to avoid deprecation notices during testing, but a follow-up issue should be filed in order to fix such tests to not exercise deprecated code.

How to deprecate

A deprecation consists of three parts:

  1. A @deprecated PHPdoc tag that indicates when the code was deprecated, when it will be removed, and what to use instead, and an @see link to the change record for the change.
  2. A @trigger_error('...', E_USER_DEPRECATED) at runtime to notify developers that deprecated code is being used. The @ suppression should be used in most cases so that we can customize the error handling and avoid flooding logs on production. In some cases, we will omit the @ if it is important to notify developers of a behavior or BC break (e.g. for a critical issue).
  3. A unit test proving the deprecation notice will be triggered when the deprecated code is called. Use a @group legacy annotation in conjunction with calls to
    $this->expectDeprecation()

Format of the deprecation message

@deprecated PHPdoc tag format

@deprecated in %deprecation-version% and is removed from %removal-version%. %extra-info%.
@see %cr-link%

@trigger_error() format

When the trigger_error() call is associated with a matching @deprecated doc tag, the format of the text is the same as the @deprecated tag:
%thing% is deprecated in %deprecation-version% and is removed from %removal-version%. %extra-info%. See %cr-link%

Where there is no associated @deprecated doc tag, the format is more relaxed to allow flexibilty in wording:
%thing% is deprecated in %deprecation-version% (free text describing what will happen) %removal-version%. %extra-info%(optional). See %cr-link%

Definitions

%thing%
What is being deprecated - for example, the class name, method name, function name, service name or the use or optional status of a parameter
%deprecation-version%
The version string representing when the change occurred.
  • For Drupal core and contrib projects that use semantic versioning, the version string is:
    • project:major.minor.patch or
    • project:major.minor.patch-tag[n]
  • For contrib projects that use legacy core-compatibility-prefixed versioning, the version string is:
    • project:8.x-minor.patch or
    • project:8.x-minor.patch-tag[n]
%removal-version%
The version string representing when the deprecated code path will be removed.
%extra-info%
This is free text. Useful things to include are hints on how to correct the code, what replacement to use, etc.
%cr-link%
The link to the change record on drupal.org (for core deprecations) or the relevant issue.

What can be deprecated

Methods

Add @trigger_error('...', E_USER_DEPRECATED) at the top of the method. Add @deprecated to the docblock for the method. For example:

/**
 * Checks if a string is safe to output.
 *
 * @param string|\Drupal\Component\Render\MarkupInterface $string
 *   The content to be checked.
 * @param string $strategy
 *   (optional) This value is ignored.
 *
 * @return bool
 *   TRUE if the string has been marked secure, FALSE otherwise.
 *
 * @deprecated in drupal:8.0.0 and is removed from drupal:9.0.0.
 *   Instead, you should just check if a variable is an instance of
 *   \Drupal\Component\Render\MarkupInterface.
 *
 * @see https://www.drupal.org/node/2549395
 */
public static function isSafe($string, $strategy = 'html') {
  @trigger_error('SafeMarkup::isSafe() is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Instead, you should just check if a variable is an instance of \Drupal\Component\Render\MarkupInterface. See https://www.drupal.org/node/2549395', E_USER_DEPRECATED);
  return $string instanceof MarkupInterface;
}

Method parameters

Add @trigger_error('...', E_USER_DEPRECATED) at the start of the code block that handles the parameter. Add an (deprecated) phpdoc parameter and detail how it is deprecated. Add an @see to the related change record to the method. For example:

    /**
     * Sets a service.
     *
     * @param string $id
     *   The service identifier.
     * @param object $service
     *   The service instance.
     * @param string $scope
     *   (deprecated) The scope of the service. The $scope parameter is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. 
     *
     * @see https://www.drupal.org/node/2549395
     */
  public function set($id, $service, $scope = ContainerInterface::SCOPE_CONTAINER) {
    if (!in_array($scope, array('container', 'request')) || ('request' === $scope && 'request' !== $id)) {
      @trigger_error('The concept of container scopes is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Omit the third parameter. See https://www.drupal.org/node/2549395', E_USER_DEPRECATED);
    }
    $this->services[$id] = $service;
  }

Constructor parameter additions

If there is an acceptable default value for the parameter (for example, for injecting a new service): Add new parameters with a default value of NULL. At the top of the constructor, check whether the parameter is NULL. If it is, set a default value and raise the @trigger_error('...', E_USER_DEPRECATED). For example:

    /**
     * Constructs a new thingamajig.
     *
     * @param FooInterface|null $foo_service
     *   The Foo service.
     */
  public function __construct(FooInterface $foo_service = NULL) {
    if ($foo_service === NULL) {
      @trigger_error('Calling ' . __METHOD__ . ' without the $foo_service argument is deprecated in drupal:9.1.0 and it will be required in drupal:10.0.0. See https://www.drupal.org/node/3078162', E_USER_DEPRECATED);
      $foo_service = \Drupal::service('namespace.foo_service');
    }
     $this->fooService = $foo_service;
   }
  }

Constructor property promotion usage example:

    /**
     * Constructs a new thingamajig.
     *
     * @param FooInterface|null $fooService
     *   The Foo service.
     */
  public function __construct(protected ?FooInterface $fooService = NULL) {
    if ($this->fooService === NULL) {
      @trigger_error('Calling ' . __METHOD__ . ' without the $fooService argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3078162', E_USER_DEPRECATED);
      $this->fooService = \Drupal::service('namespace.foo_service');
    }
   }
  }

Constructor parameter removals

For the parameters following the one being removed use a union type to declare both types. Use instanceof to determine if the deprecated parameter is passed and, if so, use func_get_arg() to initialize the parameters and raise the @trigger_error('...', E_USER_DEPRECATED). For example:

Before

  /**
   * Constructs a new thingamajig.
   *
   * @see https://www.drupal.org/node/3078162
   */
  public function __construct(FooManager $foo, BarManager $bar, BazManager $baz) {
    $this->fooManager = $foo;
    $this->barManager = $bar;
    $this->bazManager = $baz;
  }

After

  /**
   * Constructs a new thingamajig.
   *
   * @param \Drupal\Core\Foo\FooManager $foo
   * @param \Drupal\Core\Baz\BazManager|\Drupal\Core\Bar\BarManager $baz
   *
   * @see https://www.drupal.org/node/3078162
   */
 public function __construct(FooManager $foo, BazManager|BarManager $baz) {
    $this->fooManager = $foo;
    $this->bazManager = $baz;
  if ($baz instanceof BarInterface) {
    $this->bazManager = func_get_arg(2);
    @trigger_error('Calling ' . __CLASS__ . '::_construct() with the $bar argument is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. See https://www.drupal.org/node/3078162', E_USER_DEPRECATED);
  }
}

Injected service properties

First deprecate the constructor parameters as outlined above. Then use \Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait and define the removed properties and their original service ID in a new $deprecatedProperties property.

For example:

Before

class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {

  /**
   * The inbound path processor.
   *
   * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface|null
   */
  protected $pathProcessor;

After

use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;

class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    
  use DeprecatedServicePropertyTrait;

  /**
   * Defines deprecated injected properties.
   *
   * @var array
   */
  protected array $deprecatedProperties = [
    'pathProcessor' => 'path_processor_manager',
  ];

Interface and base/abstract class method signature changes

Beginning with Drupal 10, interface and base/abstract class arguments and signatures may be changed in a major release, using Symfony's DebugClassLoader.

Since it's normally a BC break to introduce new arguments (because any implementing class that does not comply with the changed signature would fail at loading), this is a two-step process.

This pattern is applicable on all argument changes. For example, changing the argument order of a method.

Step 1: Prepare the new signature

  1. During the current major release cycle, introduce the argument change(s) in an inline comment. For example:

    -  public function foo($bar);
    +  public function foo($bar /* , BazInterface $baz */);
    
    

    Or, change the typehint of an argument. For example:

    -  public function foo(string $bar);
    +  public function foo(/* string|Stringable */$bar);
    
  2. Document the changes in the docblock of the method, inside a phpcs:disable block to prevent PHPCS from firing an error when parsing the signature of the method. Include an @see to a followup issue that will implement the change in the next major release:

        * phpcs:disable Drupal.Commenting
        * @todo Uncomment new method parameters before drupal:11.0.0.
        * @see https://www.drupal.org/project/drupal/issues/3354672
        *
        * @param BazInterface $baz
        *   Documentation for parameter $baz.
        * phpcs:enable
    
  3. Tag the follow-up issue with 'Major version only'.

    This will allow the testing framework to trigger deprecation errors for the implementing classes that do not have the new signature in place yet. Since PHP allows methods to add additional non-interface arguments without errors, classes can immediately update. In order to prevent tests failures because of that, add an ignore line in .deprecation-ignore.txt. For example:

    # Drupal 11.
    %Foo::foo\(\).* will require a new "BazInterface \$baz" argument in the next major version of its interface%
    %Bar::bar\(\).* will require a new "BazInterface \$baz" argument in the next major version of its interface%
    

Step 2: Implement the new signature

Once the new major branch branch opens for development:

  1. Remove the inline comment from the interface, exposing the new full signature:

    -  public function foo($bar /* , BazInterface $baz */);
    +  public function foo($bar, BazInterface $baz);
    

    or

    -  public function foo(/* string|Stringable $bar */);
      +  public function foo(string|Stringable $bar);
    
  2. Remove the PHPCS ignore and @todo from the docblock.
  3. Implement the new signature in the concrete classes.
  4. Remove the ignore line from the .deprecation-ignore.txt file.

Procedural functions

Add @trigger_error('...', E_USER_DEPRECATED) at the top of the function. Add @deprecated to the docblock for the function. For example:

/**
 * Deletes old cached CSS files.
 *
 * @deprecated in drupal:8.0.0 and is removed from drupal:9.0.0.
 *   Use \Drupal\Core\Asset\AssetCollectionOptimizerInterface::deleteAll().
 *
 * @see https://www.drupal.org/node/2317841
 */
function drupal_clear_css_cache() {
  @trigger_error('drupal_clear_css_cache() is deprecated in drupal:8.0.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Asset\AssetCollectionOptimizerInterface::deleteAll(). See https://www.drupal.org/node/2317841', E_USER_DEPRECATED);
  \Drupal::service('asset.css.collection_optimizer')->deleteAll();
}

Return values (especially array data structures)

Generally a return value of method should not changed. However if the method returns an array it might be permissible to add new values and deprecate existing values. In this instance, it is not possible to use @deprecated because the method is not being deprecated, or @trigger_error() because you cannot determine if the deprecated key is used. Therefore it is important that the return value is fully documented and the array keys are listed and those that are deprecated are clearly listed. The method should also have an @see to the relevant change record. For example:

   * @return array
   *   An array of typed data IDs keyed by corresponding relation URI. The keys
   *   are:
   *   - 'entity_type_id'
   *   - 'bundle'
   *   - 'field_name'
   *   - 'entity_type' (deprecated)
   *   The values for 'entity_type_id', 'bundle' and 'field_name' are strings.
   *   The 'entity_type' key exists for backwards compatibility and its value is
   *   the full entity type object. The 'entity_type' key is removed from Drupal 9.
   *
   * @see https://www.drupal.org/node/2877608

(From \Drupal\hal\LinkManager\RelationLinkManager::getRelations())

Also because we want to be able to find the deprecated code to remove before Drupal 9 you need to add an @todo to #2716163: [META] Remove deprecated classes, methods, procedural functions and code paths outside of deprecated modules on the Drupal 9 branch. For example:

    // @todo https://www.drupal.org/node/2716163 Remove this in Drupal 9.0.
    foreach ($data as $relation_uri => $ids) {
      $data[$relation_uri]['entity_type'] = $this->entityManager->getDefinition($ids['entity_type_id']);
    }

Services

Add a deprecation key and message to the service definition.

  router.matcher.final_matcher:
    class: Drupal\Core\Routing\UrlMatcher
    arguments: ['@path.current']
    deprecated: The "%service_id%" service is deprecated. You should use the 'router.no_access_checks' service instead. See https://www.drupal.org/node/2317841

Further reading: https://symfony.com/blog/new-in-symfony-2-8-deprecated-service-definitions

Hooks

Mark the hook as @deprecated in the *.api.php file that documents it.

Convert the invocation of the hook to use ModuleHandlerInterface::invokeDeprecated() or invokeAllDeprecated() instead of invoke() or invokeAll(). Alter hooks should call alterDeprecated(). These methods on ModuleHandler will then call @trigger_error() if the installation has any implementations of that hook.

Add change records for deprecated hooks explaining how to accomplish use-cases without the hook.

Code paths & behavior

For example when we have a code path handling a deprecated key in a YAML file.
Add @trigger_error('...', E_USER_DEPRECATED) at the start of the code block that handles the backwards compatibility layer. DO NOT add an @deprecated phpdoc annotation to the method or function docblock that contains the code path as this causes IDEs to mark the entire method as deprecated. For example:

// @todo

Classes

Abstract classes, interfaces, traits, and classes with a private constructor

Add @trigger_error('...', E_USER_DEPRECATED) under the namespace declaration. Add an @deprecated and @see phpdoc annotations to the class docblock. For example:

<?php

namespace Drupal\datetime\Tests\Views;
	
@trigger_error('The ' . __NAMESPACE__ . '\DateTimeHandlerTestBase is deprecated in drupal:8.4.0 and is removed from drupal:9.0.0. Instead, use \Drupal\Tests\BrowserTestBase. See https://www.drupal.org/node/the-change-notice-nid', E_USER_DEPRECATED);

/**
 * Base class for testing datetime handlers.
 *
 * @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0. Use
 * \Drupal\Tests\BrowserTestBase.
 *
 * @see https://www.drupal.org/node/the-change-notice-nid
 */
abstract class DateTimeHandlerTestBase extends HandlerTestBase {

Concrete, instantiated classes

Add @trigger_error('...', E_USER_DEPRECATED) to the constructor. If there is no constructor, add one and ensure it calls the parent constructor. Add an @deprecated and @see phpdoc annotations to the class docblock. For example:

<?php

namespace Drupal\taxonomy;

use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilder;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Theme\Registry;

/**
 * View builder handler for taxonomy terms.
 *
 * @deprecated in drupal:8.4.0 and is removed from drupal:9.0.0. Use
 * \Drupal\Core\Entity\EntityViewBuilder instead.
 *
 * @see https://www.drupal.org/node/2924233
 */
class TermViewBuilder extends EntityViewBuilder {

  /**
   * {@inheritdoc}
   */
  public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager, Registry $theme_registry = NULL) {
    @trigger_error(__NAMESPACE__ . '\TermViewBuilder is deprecated in drupal:8.4.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Entity\EntityViewBuilder instead. See https://www.drupal.org/node/2924233', E_USER_DEPRECATED);
    parent::__construct($entity_type, $entity_manager, $language_manager, $theme_registry);
  }

}

Plugins

Add @trigger_error('...', E_USER_DEPRECATED) to the plugin constructor. If the plugin does not have a constructor, add one. Add no_ui = true to the plugin definition. If the plugin does not support no_ui but is selectable in a UI then an issue will be needed to add it. Also, add an @deprecated and @see phpdoc annotations to the plugin's class docblock. For example:

<?php

namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;

/**
 * Plugin implementation of the 'timestamp' formatter as time ago.
 *
 * @FieldFormatter(
 *   id = "timestamp_ago",
 *   label = @Translation("Time ago (deprecated)"),
 *   field_types = {
 *     "timestamp",
 *     "created",
 *     "changed",
 *   },
 *   no_ui = true
 * )
 *
 * @deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use the
 *   \Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampFormatter formatter
 *   instead and configure it with "Display as 'time ago'" option.
 *
 * @see https://www.drupal.org/node/2926275
 */
class TimestampAgoFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

  /**
   * Constructs a TimestampAgoFormatter object.
   * ... rest of the docs ...
   */
  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateFormatterInterface $date_formatter, Request $request) {
    @trigger_error('\Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampAgoFormatter is deprecated in drupal:8.5.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampFormatter instead. See https://www.drupal.org/node/2926275', E_USER_DEPRECATED);
    // ... constructor code...
  }

Files containing procedural functions

Add @trigger_error('...', E_USER_DEPRECATED) after the @file docblock. Add an @deprecated phpdoc annotation to the file docblock. For example:

/**
 * @file
 * Miscellaneous functions.
 *
 * @deprecated in drupal:8.3.0 and is removed from drupal:9.0.0.
 *   See each function in the file for individual deprecation notices with
 *   upgrade instructions.
 *
 * @see https://www.drupal.org/node/12345678
 */
@trigger_error(__FILE__ . ' is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. See individual methods in the file for individual deprecation notices with upgrade instructions. See https://www.drupal.org/node/12345678', E_USER_DEPRECATED);

Unintended behavior and security issues

For cases where using the previous API could result in unintended behavior or security issues, we may deliberately break backwards compatibility. Omit error suppression in such cases, so that developers can find and fix code that may no longer work after the BC break. For example:

elseif (strpos($string, $key) !== FALSE) {
  trigger_error("Fallthrough for unrecognized placeholders to %variable is deprecated in drupal:8.2.0 and is removed from drupal:8.2.0. Invalid placeholder ($key) in string: $string. See https://www.drupal.org/node/2605274', E_USER_DEPRECATED);
}

In the code above from #2807705: FormattableMarkup::placeholderFormat() can result in unsafe replacements we removed the ability to replace placeholders that started with an alpha character. The replacement was done without sanitization and opened possible security issues, so rather than maintain backwards compatibility, we stopped doing the replacement altogether. Therefore, we trigger an error without suppression so contributed projects, custom code, and real sites are alerted immediately to the problem and could fix any errant placeholders.

Deprecating internal APIs

Anything considered internal API according to the backwards compatibility policy does not strictly require BC. However, as a best practice, we will still deprecate internal APIs first to reduce disruption and make the process easier to understand.

To make it clear that deprecated internal APIs are still internal code without full BC support, the deprecation notice should have the following format:

baz() is deprecated in drupal:8.3.0 and is removed from drupal:9.0.0. Use \Drupal\Foo\Bar::baz() instead. As internal API, baz() may also be removed in a minor release.

Note that it is not required to have a change record linked in the message, since internal API changes do not always have a change record. (Some internal API changes do have change records if they are significant enough, and if so, the link to the change record should be included in the normal fashion.)

The patch that adds the deprecation should also ensure that the code is explicitly marked as @internal in the PHP docblock.

Backwards compatibility layers for @internal code also generally do not require test coverage for the deprecated code path.

Asset Libraries

Drupal's *.libraries.yml may now add a deprecated key to an asset library to indicate that the asset library is deprecated and will be removed in the next major version. This key should contain the deprecation information in the standard format, and may use the token %library_id% as a placeholder for the library name. The LibraryDiscovery service will then trigger an error if this library is accessed.

Tests of the deprecation message are not required when using the 'deprecated' key.

For example:

jquery.ui.effects.scale:
  version: *jquery_ui_version
  license: *jquery_ui_license
  js:
    assets/vendor/jquery.ui/ui/effects/effect-scale-min.js: { minified: true }
  dependencies:
    - core/jquery.ui.effects.core
  deprecated: The "%library_id%" asset library is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Require jQuery UI as an explicit dependency or create a pure JavaScript solution. See https://www.drupal.org/node/3064015

JavaScript

A @deprecated JSDoc tag that indicates when the code was deprecated, when it will be removed, and what to use instead, and an @see link to the change record for the change.

The JSDoc text format should be formatted like this:

@deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use Drupal.theme.fooBar() instead. 

@see https://www.drupal.org/node/the-change-notice-nid

To trigger JavaScript deprecation errors from arbitary code, use Drupal.deprecationError():

Drupal.theme.div = function ($elements) {
    Drupal.deprecationError({
      message: 'The Drupal.theme.div is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. See https://www.drupal.org/node/2575199.'
    });
    return $('<div></div>');
};

To trigger deprecation errors for deprecated properties, use Drupal.deprecatedProperty(). This notifies at runtime developers that deprecated code is being used.

Drupal.deprecatedProperty({
  target: { some_property: 'value', someProperty: 'value' },
  deprecatedProperty: 'property',
  message: 'The some_property property has been deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use someProperty instead.',
});

JavaScript deprecation errors can be found in the browser console. However, JavaScript deprecation errors are suppressed by default. The suppression can be disabled by adding the following code to a module:

function hook_js_settings_alter(&$settings) {
  $settings['suppressDeprecationErrors'] = FALSE;
}

JavaScript deprecation errors can be also tracked in automated tests. JavaScript deprecation errors will be translated into PHP @trigger_error calls on WebDriver based tests.

On Nightwatch based tests, it is recommended to assert that the test scenario didn't run into deprecated code paths by adding the following assertion to the end of each test scenario:

browser.assert.noDeprecationErrors();

We follow the same general best practices for JavaScript deprecations as we do for PHP, namely: public APIs should always provide BC and a deprecation. Internal APIs do not require BC and deprecations, but it's recommended to provide them unless there are substantial costs to doing so (in terms of maintainability, performance, etc.).

Settings used by Drupal core

As of Drupal core 9.1.0 Core settings keys can be deprecated. Settings are managed by the \Drupal\Core\Site\Settings class (see core/lib/Drupal/Core/Site/Settings.php).

To do this, add the information to the Settings::$deprecatedSettings array. The information is keyed by the deprecated setting name. The value is an array with 2 keys:

replacement
The new setting name.
message
The deprecation message triggered when the deprecated setting is configured in settings.php and when Settings::get() is used to get the value for the deprecated setting.

For example:

  private static $deprecatedSettings = [
    'old_setting' => [
      'replacement' => 'new_setting',
      'message' => 'The "old_setting" setting is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use "new_setting" instead. See https://www.drupal.org/node/CR-NID',
    ],
  ];

If the settings.php file for a site (or any file it includes) contains the legacy setting name (continuing the example above, 'old_setting'), the deprecation warning will be generated. If code calls Settings::get('new_setting') but 'new_setting' is not yet defined, the value of 'old_setting' will be returned.

If any code calls Settings::get() with the legacy name, the deprecation warning will also be generated. If the replacement setting is already defined, Settings::get('old_setting') will return the value of new_setting.

Only once the legacy setting has been removed from settings.php and all calls to Settings::get('old_setting') have been removed will a site stop generating these deprecation warnings.

Since settings are used very early in a Drupal request (e.g. before Drupal can connect to a database, build a service container, load modules, etc.), there's no way for contributed extensions to utilize the same mechanism to deprecate settings they introduce. Contributed extensions need to deprecate settings in the code that uses the setting (the spot where the extension calls Settings::get() to retrieve a value). See Code paths & behavior above for more.

Configuration schema

Add a deprecated property in the deprecated config schema entry. The value should be the deprecation message. For example:

complex_structure:
  type: mapping
  label: Complex
  deprecated: "The 'complex_structure' config schema is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Use the 'complex' config schema instead. See http://drupal.org/node/the-change-notice-nid."
  mapping:
    key:
      type: ...
    ...

Theme templates

Add an item with deprecated key in the deprecated hook_theme entry. The value should be the deprecation message. For example:

/**
  * Implements hook_theme().
  */
function node_theme() {
  return [
    'old_template' => [
      'variables' => ['content' => NULL],
      'deprecated' => 'The "old_template" template is deprecated in drupal:10.2.3 and is removed from drupal:11.0.0. Use "new_template" instead. See https://www.drupal.org/node/123456',
    ],
  ];
}

Modules and themes

Modules and themes in Drupal core can sometimes be deprecated in their entirety, in order for the module to removed from core in the next release.

There are broadly two routes for a module to be removed:

  1. The functionality of the module is incorporated in to other core modules or the base system.
  2. The module in its entirety is moved to a contributed project

In the first case, when a module is incorporated into other core modules, any APIs it provides should be individually deprecated. The module can then be marked obsolete and an update added to uninstall it. Once a module has been marked obsolete, it can then be removed in the next major release, where the update to uninstall it must be removed (incrementing hook_update_last_removed() to ensure it is run before the module is uninstalled).

An example of this is when the field type provided by entity_reference module was moved to a core field type, leaving the module empty and safe to uninstall.

Follow the steps in Remove a core module by incorporating it other core module to complete the process.

In the second case, when a module or theme is to be moved to a contributed project, the following steps should be taken, this may not be an exhaustive list since each module is different:

  1. Discuss module removal
    1. Open an issue against the Drupal core ideas providing justification for removing the module.
    2. Seek approval from product managers.
    3. There must be someone willing to maintain the module or theme in contrib (at least to co-ordinate security releases while it is still in a supported core version), and a contributed project should be made with the same namespace
  2. When the above issue is marked Fixed and the decision is to remove the module from core, then follow the steps in Remove a core module and move it to a contributed project to complete the process.

Help improve this page

Page status: No known problems

You can: