Advertising sustains the DA. Ads are hidden for members. Join today

How to deprecate

Last updated on
17 May 2026

Introduction

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

This document explains how to deprecate most code and what can be deprecated. For extension deprecation and removal refer to How to deprecate and remove an extension 

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

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).
    1. Use PHP magic constants to declare what is being deprecated in the message.
  3. Tests.
    1. A test is only required when there is backwards compatibility (BC) logic that needs to be tested.
      Examples:
      1. Add a test when
        1. There are multiple code paths and the code that is not considered '@internal' according to the BC policies.
      2. Do not test;
        1. A constructor BC.
        2. That the deprecation error is triggered.
    2. When adding a test, use a #[IgnoreDeprecations] attribute 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. In some cases, this is a link to the issue. Read the section below to determine if the deprecation being worked on should use an issue link.

What can be deprecated

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

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(__CLASS__ . ' 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);
  }

}

Class properties

Add @trigger_error('...', E_USER_DEPRECATED) to both the __get() and __set() magic methods. If the methods do not exist, then add them. For example:

Before

  /**
   * Should a block header be shown?
   */
  public $show_header = TRUE;

After

  /**
   * Should a block header be shown?
   *
   * @var bool
   */
  public $showHeader = TRUE;
  
  /**
   * {@inheritdoc}
   */
  public function __get(string $name) {
    if ($name === 'show_header') {
      @trigger_error('Accessing the $show_header property is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Component\Diff\DiffFormatter::$showHeader instead. See https://www.drupal.org/node/1234', E_USER_DEPRECATED);
      return $this->showHeader;
    }
  }
  
  /**
   * {@inheritdoc}
   */
  public function __set(string $name, $value): void {
    if ($name === 'show_header') {
      @trigger_error('Accessing the $show_header property is deprecated in drupal:10.3.0 and is removed from drupal:11.0.0. Use \Drupal\Component\Diff\DiffFormatter::$showHeader instead. See https://www.drupal.org/node/1234', E_USER_DEPRECATED);
      $this->showHeader = $value;
      return;
    }
  }

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

Composer metapackage

Add a abandoned key to the package composer.json and the associated builder class. Change the deprecation message to begin with "Deprecated" and end with instructions for what to do instead. For example:

   "description": "Deprecated. Pinned require-dev dependencies from drupal/drupal; use in addition to drupal/core-recommended to run tests from drupal/core. Use drupal/core-dev instead to avoid security vulnerabilities from pinned versions.",
    "abandoned": "drupal/core-dev",

Add @deprecated to the docblock for the builder class. For example:

/**
 * Builder to produce metapackage for drupal/core-dev-pinned.
 *
 * @deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use
 *   drupal/core-dev instead.
 *
 * @see https://www.drupal.org/node/3566742
 */

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: ...
    ...

Constants

Add @deprecated to the docblock for the constant. For example:

/**
   * Flag for dealing with existing files: Replace the existing file.
   *
   * @deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Use
   * \Drupal\Core\File\FileExists::Replace instead.
   *
   * @see https://www.drupal.org/node/3426517
   */
  const EXISTS_REPLACE = 1;

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). A change record is not required so use the link to the issue in the message. 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/project/drupal/issues/1234567', 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 ttps://www.drupal.org/project/drupal/issues/1234567', 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). A change record is not required so use the link to the issue in the message. 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 ttps://www.drupal.org/project/drupal/issues/1234567', E_USER_DEPRECATED);
  }
}

Extensions (modules and themes)

Modules and themes in Drupal core can be deprecated and removed from core in the next major release. There are two options.

Incorporate the functionality of the module into core

Each API provided by the extension should be deprecated. The extension can then be marked as 'obsolete'. Once a module is 'obsolete', it can 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. The module was then empty and safe to uninstall.

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

Move the extension to a contributed project

The following steps should be taken, this may not be an exhaustive list since each extension is different.

  1. Discuss module removal
    1. Open an issue against Drupal core 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.

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);

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.

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

This is a two step process, spanning two major versions. Step 1 is implemented in the current major version and step 2 in the next major version.

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, 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. Add a phpcs:ignore Drupal.Commenting.FunctionComment.ParamNameNoMatch before the @param for the additional parameter. This prevents PHPCS from reporting an error when parsing the signature of the method. And an @see to the followup issue that will implement the change in the next major.  Add an @todo explaining to uncomment the additional parameter in the next major release.  Example:

     /**
       * Returns sample data.
       *
       * phpcs:ignore Drupal.Commenting.FunctionComment.ParamNameNoMatch
       * @param string $added_parameter
       *   Documentation for the added parameter.
       *
       * @return string[]
       *   The sample data.
       *
       * @see https://www.drupal.org/project/drupal/issues/3354672
       *
       * @todo Uncomment the new $added_parameter method parameter before drupal:12.0.0.
       */
      public function getSampleData(/* string $added_parameter */);
  3. Tag the follow-up issue with 'Major version only'.

    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%
    

    This allows the testing framework to trigger deprecation errors for classes that are not using the new signature in place. Classes can update immediately because PHP allows methods to add additional non-interface arguments.

Step 2: Implement the new signature

Once the next major 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.

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.

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.).

JavaScript and CSS libraries

The deprecation_link is a link to the change record for the issue that made the change.

drupal.comment:
  version: VERSION
  js:
    js/comment-entity-form.js: {}
  dependencies:
    - core/jquery
    - core/drupal
    - core/drupal.form
  moved_files:
    comment/drupal.comment:
      deprecation_version: drupal:11.1.0
      removed_version: drupal:12.0.0
      deprecation_link: https://www.drupal.org/node/3471539
      js:
        comment-entity-form.js: 'js/comment-entity-form.js'

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(__METHOD__ . '() 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 has no effect in 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 has no effect in drupal:9.0.0. Omit the third parameter. See https://www.drupal.org/node/2549395', E_USER_DEPRECATED);
    }
    $this->services[$id] = $service;
  }

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(__CLASS__ . ' 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...
  }

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(__FUNCTION__ . '() 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();
}

Rename a class, interface, or trait

For most cases, use the following steps to preserve compatibility with IDE class indexes and static analysis tools like phpstan:

  1. Rename the class to the new class name.
  2. Re-create the old class as an empty class, and extend from the new class.
  3. Add an @deprecated tag to the class comment, linking to the new class and noting the version the class was deprecated in and in what version it will be removed.

If a class cannot be renamed (such as if there is a performance issue maintaining the old classes, or if the class is marked final), add a *.moved_classes parameter to the relevant *.services.yml file.

Example:

parameters:
  module_autoload_test.moved_classes:
    'Drupal\module_autoload_test\Foo':
      class: 'Drupal\module_autoload_test\Bar'
      deprecation_version: drupal:11.2.0
      removed_version: drupal:12.0.0
      change_record: https://www.drupal.org/node/3521054

The deprecation_version, removed_version, and change_record may be omitted in rare cases. One example is when the class is used in serialized data which cannot be easily updated.

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())

Add an @todo with a URL directing to the relevant issue for removing deprecations. The issue will be title like, "[META] Remove deprecated classes, methods, procedural functions and code paths outside of deprecated modules on the ...'. For Drupal 9 the issue was 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']);
    }

Routes

Since Drupal 11.2, a route can be deprecated.

Add a deprecation key to the route definition. And add the package, version and message information.

route:
  alias: other_route
  deprecated:
    package: 'drupal/core'
    version: '11.2.0'
    message: 'The route "route" is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use "other_route" route instead. See https://www.drupal.org/node/3317784'

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 in drupal:10.1.0 and is removed from drupal:11.0.0. 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

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.

Template variables

Template variables can be deprecated by adding to the $variables['deprecated'] array, with the key as the variable name and the value as the deprecation message. The deprecation should be defined where the variable is defined so that it's easy to remove both at the same time, so either in hook_theme() or template_preprocess_HOOK().

function my_custom_module_theme($existing, $type, $theme, $path) {
  $items['custom_template'] = [
    'variables' => [
      'result' => NULL,
      'new_result' => NULL,
      'deprecations' => [
        'result' => "'result' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_result' instead. See https://www.example.com."
      ]
    ],
  ];
  return $items;
}
function template_preprocess_foo(&$variables) {
  $variables['result'] = 'foo';
  $variables['deprecations']['result'] = "'result' is deprecated in drupal:X.0.0 and is removed from drupal:Y.0.0. Use 'new_result' instead. See https://www.example.com."

  $variables['new_result'] = 'foo';
}

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',
    ],
  ];
}

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.

Help improve this page

Page status: No known problems

You can: