Deprecation guidelines

Deprecation is the process by which we refactor and clean up old code, getting rid of functions, classes, methods, files and resources that are no longer used.

We have deprecation guidelines because even if we clean up all uses of something we cannot just remove it, as we're an open source project and we market ourselves as such to those who use our software. It's important to assume that someone out there is using every single function, class, method, file, and resource in their own plugins and customisations.

Overview and basics

These guidelines are to ensure that there is consistency in how we clean up code, and to ensure that those who build upon our system are given fair opportunity to upgrade their code before everything breaks. Only once something has been deprecated for period of time can it be removed from existence.

Anything which might break customisations or third-party plugins must be properly deprecated (and communicated!) for at least one major release, in order to give our partners time to upgrade their code.

Deprecation timeline

By default the removal of functionality will be considered for the next major release after the major release for which it was marked as deprecated. For example, a function marked as deprecated for the Totara 17 release will be removed for the Totara 18 release.

There are a few exceptions to this, most notably the deprecation policy only needs to be followed for code structures and resources that are part of the public API. If the method or property is private, or if it is marked with the PHPDoc @internal tag, or the PHPDoc comment clearly identifies it as private API, then any change can be made at any time. 

The upgrade.txt file

As of Totara 10 we have started recording all technical changes and deprecations within upgrade.txt files. Each component and plugin can have its own upgrade.txt file. The file should be located in the plugin's root folder (same location as the version.php file).

Entries to the upgrade.txt file are made at the same time as the code is changed. This means that during review and testing you should be looking for technical changes that should be noted and ensuring they are indeed noted in the upgrade.txt in that very same patch. 

In Totara 9 and below we record these details in the Changelog details field on the JIRA issue. In Totara 10 the Changelog details field is used only to provide details on the actual change, not what has changed technically.

The following is an example of the upgrade.txt file:

This file describes API changes in Totara Connect Server, information provided here is intended for developers. === 11 ===   * create_something() the first argument is no longer used. Functionality has not changed. * some_class::some_method() has been deprecated, please call some_class::some_other_method() instead.   === 10 === * create_function() added second argument $bar * some_class has been renamed to \totara\core\some_class, please update all uses.  

If you are making a technical change then please ensure you record a summary of that change in this file. You can put what ever technical notes you want into it, but please make sure you follow a consistent format. If the area you are making the change in does not already have an upgrade.txt file then please create one and add your notes as part of your patch. 

The PHP debugging call

Wherever executable PHP code is deprecated, we add a call to debugging() with the so that we can confirm that the deprecated code is no longer being executed by unit and behat tests. These notices also appear in logs so that partners and third-party developers can tell when they have customisations that are going to stop working in a future version of Totara.

Include the debugging() call before any other logic in the deprecated function or method.

/** * Description here. * * @deprecated since Totara 19.0 * @param stdClass $context the context * * @return bool */ public function do_something($context) { debugging(__METHOD__ . ' has been deprecated; please use new_method() instead', DEBUG_DEVELOPER); return (bool) $this->new_method($context); }

Deprecating language strings

This topic has its own page: Deprecating Language Strings which should be folded into this one in a future update.

Please note that there is an issue when the deprecated langstring is used in a deprecated Vue component, it will still be included in the compiled Tui bundle. This will cause a usage log message that will fail behat tests.

Changing function or method arguments

The signature of a function or method must remain constant through its entire existence, existing arguments can never be removed or reused for something else, any new information required must be passed via a brand new argument added to the end of the arguments list.

It is imperative that the above is always observed, never remove an argument and never reuse an argument for something different.
Doing the above makes life incredibly hard for third party contributors and can lead to impossible to maintain code in the future. In a worst case scenario reusing an argument can lead to data loss issues and even security issues, especially for those who write plugins and customisations.

The following sections describe how to make the more common argument changes to a function or method, but please note there are many different changes that are required less frequently.
The key rule that all of these changes must observe is that any existing calls to the function must continue to work after your change. Backwards compatibility is of the utmost importance. 

Adding a new argument

When adding an argument there are a couple of key things you must ensure are done:

  1. The new argument must be added as the last argument.
    Nothing goes in the middle, always at the end. Adding to the middle is a cardinal sin! 

  2. It must have a default value.
    This is required because any existing calls to the function will not be passing this argument. 
    The default value must lead to the correct default behaviour.

  3. The version it was added to should be noted in the PHPdocs.

The following illustrates the best practice when adding an argument to a function or method. Pay close attention to the PHPdoc syntax changes as well. 

Original:

/** * This function creates something. * * @param string $foo a summary of this variable. */ function create_something($foo) { // ... }

With the added argument:

In upgrade.txt:

Notes:

  • The purpose of a function should never change. If the purpose of the function is changing then you should deprecate the old function and create a new one.
    Make the old function call the new one if that is possible.

  • If you need to add an argument that must be provided then you should not be adding an argument at all. You should be writing a new function/method. 

Removing an argument

Function arguments must never be removed.
On occasion you will encounter a situation where an argument is no longer required. Despite its place in the definition, it must never be removed, even if it is the last argument.
If you do find your changes render an argument unused then you should do the following:

  • Rename the variable to $unused or similar.

  • Make sure it has a default value of a simple type for example null or false

  • Amend the PHPdocs to reflect that this variable is unused.

  • If appropriate add a debugging call if the unused variable does not match its default value.

The original function:

With the removed argument:

In upgrade.txt:

Notes:

  • The debugging call should only be present if functionality has changed. As functionality should be changed only in rare situations this debugging notice should be equally rare.

  • The purpose of a function should never change. If the purpose of the function is changing then you should deprecate the old function and create a new one.
    Make the old function call the new one if that is possible.

Changing scope; public, protected, private

On occasion you will need to change the scope of a method or property.

The direction that you are changing it determines what needs to be done.

Absolutely all changes to scope MUST be mentioned in an upgrade.txt file

Widening scope of a method

If you are widening the scope, changing it to from private to protected, or protected to public, then you can make this change in code and simply note the scope change in the upgrade.txt as below:

If you are restricting the scope of a method or property then you also must ensure it is made publicly accessible for a period of time no less than 1 major release.

Narrowing scope of a method

On occasion you may find yourself converting a public method to either protected or private. Typically this done to clean up an API.

This must always be done by deprecating the old method, but we can use the __call() magic method to do so:

The original code:

Converted to a protected method:

In upgrade.txt:

Notes:

  • __call() should throw a debugging notice to inform the caller of the deprecated function as well how any public calls should be upgraded to.

  • On stable branches the function must continue to function exactly as it did previously. This is also preferable on the development branch, although some leniency can be applied in extreme edge cases.

Converting a public property to a private property

On occasion you may find yourself converting a public property to either protected or private. Typically this done to clean up an API.

The technique is similar to narrowing a method scope, using the magic __get() and __set() methods to provide deprecated access to the property.

The original code:

Converted to a private property:

In upgrade.txt:

Notes:

  • __get() should throw a debugging notice to inform the caller of the deprecated property as well as what they should do instead.

  • __set() should:

    • throw a debugging notice and inform the user of the alternative (if there is one)

    • continue to allow setting of $bar unless it is imperative that it does not, in which case it should throw a coding exception instead.

Removing a function, method or class

It is important functions, methods and classes are never immediately removed from code.

Instead they should be deprecated for at least 1 major release before being removed.

Where possible they should be kept in a functioning state during the deprecation period, to ensure any third party calls to them continue to work.

Situations where it is not possible to keep the old function, method, or class working should be extremely rare.

In these situations it is advisable to throw a coding exception, ensure that upgrade.txt is appropriately helpful, and ensure that the "Changelog details"  mentions that there are PHP API changes that are not backwards compatible.

In upgrade.txt:

Removing a unit test related to a deprecated method or class

Because deprecated methods are still in the product and can be used by third-party developers, they still need to be maintained. Therefore they need to be covered by tests. 

If all logic is removed from a deprecated method or class, so that it exists as a stub that is calling new replacement method(s) that are covered by tests, then the covering tests may be able to be removed as redundant. 

If not, then each method in the testcase which calls a deprecated method must be modified with the static::assertDebuggingCalled(...); and/or static::assertDebuggingCalledCount(...) calls:

Modifying the unit test:

Note that Behat features should never trigger debugging() calls, because a properly-deprecated method is not used anywhere in our product. It would only be used by third-party customisations.

Removing a library file

Files are a little different to functions, methods and classes.

It is important that the structures present in a file are handled as per this guidelines.

The file should not be removed immediately, otherwise any code that tries to include it will encounter an error.

Instead it should be deprecated in a similar way to other structures and removed after it has been deprecated for at least one major release.

Contents of the deprecated file:

In upgrade.txt:

Removing an entry point file

Files are a little different to functions, methods and classes.

It is important that the structures present in a file are handled as per this guidelines.

The file should not be removed immediately, otherwise any in-content links, bookmarked URLs, or navigation menu items that expect the entry point to be there will fail with a 404 error.

Instead, the old entry point should be deprecated in a similar way to other structures and removed after it has been deprecated for at least one major release.

Contents of the deprecated file:

In upgrade.txt:

Renaming a class

Needs more detail - should mention the db/renamedclasses.php file and provide an example of how this is done.

In upgrade.txt:

Renaming a function

Remember a function should never be removed, as such you need to ensure the old function still works but is marked as deprecated.

The original code:

Converted to a private property:

In upgrade.txt:

Renaming a method

Remember a method should never be removed, as such you need to ensure the old method still works but is marked as deprecated.

The original code:

Converted to a private property:

In upgrade.txt:

Moving a file

This should never be done, create the new file and deprecate the old file as per the "Remove file" guideline, the old file should show the debugging notice before including the new file.

Moving a function, or class to a new file

Put the function or class in the new location, and then include the new file from the old file.

This should not be done straight up, you should rename the function or class at the same time.

Changing a renderer

Renderer changes should be kept to a minimum, remember we should only be fixing bugs in stable branches, UI changes should be rare. 
Please ensure the UX team have been involved in all UI changes.

In upgrade.txt:

In the above, the first is an example of a bug fix.
The second is an example of changing a template per a design spec. 

Changing a template

Template changes should be kept to a minimum, remember we should only be fixing bugs in stable branches, UI changes should be rare. 

Please ensure the UX team have been involved in all UI changes.

In upgrade.txt:

In the above, the first is an example of a bug fix.

he second is an example of changing a template per a design spec. 

Changing the context object used by a template

We have to be extremely cautious when changing context objects, they are a part of the public API.

As such when changing a context object we must ensure the previous object properties are kept and deprecated for a release before being removed. 

In upgrade.txt:

Changing a GraphQL attribute type

GraphQL attributes should not be changed. Instead create a new attribute with a new name and mark the old attribute as deprecated with a comment explaining if there's a replacement.

In schema.graphqls

In webapi/resolver/type/item.php

In upgrade.txt:

Changing an existing GraphQL persisted query

Deprecation of GraphQL attributes may result in the need to modify persisted queries in the codebase in order to stop using the deprecated fields. It is acceptable to modify an existing persisted query (rather than creating a new version) with the following caveats:

  1. The change occurs in a new major release (not a point release).

  2. The change to an existing persisted query is well documented, in the upgrade.txt for the component, changelogs, and extended release notes.

Theme developers should read the major release changelogs and release notes when updating their themes to support new major versions, and fix the use of any core persisted queries that have changed.

Changing TUI framework files and components

TUI framework code can be classified into two types:

  1. TUI Core (the TUI framework itself)

  2. TUI Plugins (functional implementations that likely depend on TUI Core)

TUI Core

TUI Core is considered a public API, and should follow appropriately detailed steps as noted above for back-end deprecation. This is because TUI Plugins have many dependencies upon TUI Core, and TUI Core also has internal dependencies, e.g. Uniform and its collection of Form components and utilities.

Whole files should remain in place and be noted as deprecated in upgrade.txt and changelogs, and can be removed after being deprecated for at least one major release.

In *.js utility files

Exported methods should be marked with a @deprecated comment, and can be removed after being deprecated for at least one major release.

In *.vue files

Similar to *.js files, marking methods, props and data structures as @deprecated and updating upgrade.txt and changelogs appropriately is also required for TUI Core components. This includes breaking changes to <template /> , <script /> , <style /> and <lang-strings />  code blocks within a Single-File Component (SFC). 

TUI Plugins

To avoid cross-plugin dependencies, TUI Plugins should ideally not depend on each other, and should only be used within the context in which they need to exist when the Plugin is developed. For example, client/component/totara_engage/src/components/sidepanel/NavigationPanel.vue should not be reused within client/component/mod_perform/src/pages/Activities.vue (because the common functionality should be abstracted into TUI Core client/component/tui/src/components/sidepanel/* ).

If there are no cross-plugin dependencies for a TUI Plugin, then the deprecation of .vue entry pages, component trees, component internal code blocks and Javascript utilities can follow a less stringent deprecation process, for example the original implementation does not need to be left in place. If there are cross-plugin dependencies then the TUI Core deprecation steps should be taken, and a refactor to abstract reusable logic and components considered for prioritisation in order to remove the cross-plugin dependency.

Theme-based TUI component overrides

Totara TXP 13+ supports per-code-block overrides as described in the TUI front-end developer documentation. This feature can reduce the amount of work involved in maintaining Vue component customisations. It does, however, introduce a dependency risk, where the TUI Core or TUI Plugin component being overridden may contain deprecations. Therefore developers will need to be conscious of changelogs, upgrade.txt entries and Git history to understand the impact of deprecation on Theme-based overrides.