Error Handling and Legacy APIs

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

Joomla grew up on PHP 4, which had no exceptions, so the CMS invented its own error protocol: a method records a message with setError(), returns false, and the caller is expected to ask getError() what went wrong. Twenty years later that protocol is still wired into models, tables, users and the installer, together with a layer of old class names, constants and helper packages that the modern code base no longer needs. This page explains how the protocol works, how to move an extension off it, and which legacy APIs disappeared in Joomla 6.0 or are scheduled to disappear in 7.0. The example extension is the fictitious Tallpine Hiking Club package pkg_tallpine with the component com_tallpine (namespace Tallpine\Component\Tallpine).

It was written against Joomla 6.2.x on PHP 8.3+ and checked on 2026-10-30. Joomla 6.2 is the current release, 5.x is in its security-only period (5.4.10 was the last bugfix release; security fixes continue until 2027-10-12) and 4.x is end of life. The chapters of the official Manual that this page is built on are the 5.4 to 6.0 update notes Error handling, Removed and Backward Incompatibility, Compatibility Plugins and New deprecations. For the bigger picture see Deprecations and Preparing for Joomla 7 and Upgrading Joomla 5 to 6. PHP’s own exception model is not repeated here; see Errors and Exceptions.

Two error protocols in one code base

Joomla 6.2 supports two ways to report a failure, and a class may use either:

Legacy protocol Exceptions

Reporting

$this->setError('message') and return false;

throw new \RuntimeException('message');

Reading

$model->getError(), $model->getErrors() after a call returned false

try { ... } catch (\Exception $e)

Cost of forgetting

A silent failure: the caller ignores the false and continues with bad data

A visible failure: the exception reaches Joomla’s error page or the log

Status in 6.2

Deprecated; core still contains it and still supports it

Preferred everywhere; the only protocol that will remain after 7.0

The legacy protocol lives in one place, the trait \Joomla\CMS\Object\LegacyErrorHandlingTrait. It was introduced in Joomla 4.3 when the error methods were extracted from CMSObject, so that classes which only needed error handling could stop extending CMSObject. In the Joomla 6.2 source it is used by BaseModel, AbstractView (and through them every model and every view), Table, User, Installer, InstallerExtension and Update, and by CMSObject itself.

The trait holds a protected $_errors array and the methods getError(), getErrors() and setError() (all marked deprecated since 3.1.4, to be removed in 7.0 without replacement), plus two methods added in Joomla 5.4: shouldUseExceptions() and setUseExceptions(bool $value).

The switch added in Joomla 5.4

Because core has to support extensions that read getError() as well as extensions that expect exceptions, the 5.4 release added a per-object flag. The behaviour is deliberately small:

  • setUseExceptions(true) turns the flag on for that object. shouldUseExceptions() reads it. The default is false, so nothing changes for code that does not opt in.

  • With the flag on, setError() with a string argument throws a generic \Exception with code 500 instead of appending to $_errors. An \Exception object passed to setError() is still just recorded.

  • Core code that was converted checks the flag first and throws a specific exception, and only otherwise falls back to setError() and return false.

  • BaseDatabaseModel::getTable() passes the flag on to the table it creates, so a model with exceptions enabled produces tables with exceptions enabled.

The list and form views of core components call $model->setUseExceptions(true) themselves and wrap the data loading in try/catch, converting any exception into a GenericDataException. This is the shape that a view of com_tallpine should have too:

<?php

namespace Tallpine\Component\Tallpine\Administrator\View\Trails;

use Joomla\CMS\MVC\View\GenericDataException;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;

\defined('_JEXEC') or die;

class HtmlView extends BaseHtmlView
{
    protected $items;
    protected $pagination;
    protected $state;

    public function display($tpl = null)
    {
        $model = $this->getModel();
        $model->setUseExceptions(true);

        try {
            $this->items      = $model->getItems();
            $this->pagination = $model->getPagination();
            $this->state      = $model->getState();
        } catch (\Exception $e) {
            // Keep the original exception as "previous" so that the log has the real cause.
            throw new GenericDataException($e->getMessage(), 500, $e);
        }

        parent::display($tpl);
    }
}

Before 5.4 the same view ended with the pattern that older books and tutorials still show: if (count($errors = $this->get('Errors'))) { throw new GenericDataException(implode("\n", $errors), 500); }. That code runs, but it only reports errors that the model remembered; an exception thrown inside the model, or an error that a careless model forgot to record, passes it by.

Timeline of the removal

The official plan is spread over three majors, so that extensions can migrate whenever it suits them:

flowchart LR A["Joomla 4.3<br/>LegacyErrorHandlingTrait extracted from CMSObject"] --> B["Joomla 5.4<br/>setUseExceptions and shouldUseExceptions added"] B --> C["Joomla 6.0 to 6.x<br/>core converted step by step, both protocols work"] C --> D["Joomla 7.0<br/>core stops calling setError, flag checks removed, CMSObject removed"] D --> E["Joomla 8.0<br/>LegacyErrorHandlingTrait itself dropped"]

In Joomla 7.0 the core removes its own calls to setError() and its checks of shouldUseExceptions(). Whether shouldUseExceptions() should then default to true is described by the Manual as still under discussion. The trait is planned to be dropped in 8.0. The @deprecated tags in the 6.2 source say the same: the error methods are removed in 7.0, and the useExceptions property and its two accessors are marked as deprecated for 7.0.

In Joomla 6.0 two classes that carried the trait without using it were cleaned up: CategoryNode and Changelog no longer include LegacyErrorHandlingTrait. An extension that subclassed them and called setError() on the subclass has to add the trait itself or, better, stop calling it.

What a third-party developer should do

The core examples look elaborate because core has to serve both camps at once. The Manual’s advice for extensions is simpler: switch to exceptions completely and force the flag on in the objects you extend. A table class of com_tallpine shows it:

<?php

namespace Tallpine\Component\Tallpine\Administrator\Table;

use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseInterface;

\defined('_JEXEC') or die;

class TrailTable extends Table
{
    public function __construct(DatabaseInterface $db)
    {
        parent::__construct('#__tallpine_trails', 'id', $db);

        // Core classes that we extend must throw as well. Remove this line when you drop support
        // for Joomla versions that still have the legacy protocol (the trait goes away in 8.0).
        $this->setUseExceptions(true);
    }

    public function check()
    {
        if (trim((string) $this->title) === '') {
            // Legacy style: $this->setError(...); return false;
            throw new \UnexpectedValueException('COM_TALLPINE_ERROR_TRAIL_TITLE_REQUIRED');
        }

        if ($this->distance_km !== null && (float) $this->distance_km < 0) {
            throw new \RangeException('COM_TALLPINE_ERROR_TRAIL_DISTANCE_NEGATIVE');
        }

        return parent::check();
    }
}

Rules of thumb that follow from the Manual and from how the flag is propagated:

  • Call setUseExceptions(true) in the constructor of the object, not at some later point, because internal calls made by the constructor or by the parent class would otherwise still use the legacy path.

  • If your object creates other objects that use the trait (a model creating tables, a controller creating models), pass the current shouldUseExceptions() value to them. getTable() does it for you; a model created with MVCFactory::createModel() does not inherit anything.

  • Throw specific exceptions (\InvalidArgumentException, \RuntimeException, your own subclasses) instead of the generic \Exception that setError() produces, and pass the original as the third argument when wrapping.

  • Do not mix: a method that sometimes returns false and sometimes throws is harder to use than either extreme. Document the exceptions with @throws.

  • Code that must still run on Joomla 4.x or on 5.x before 5.4 cannot call setUseExceptions(): on those versions the method does not exist. Guard it with method_exists($object, 'setUseExceptions'), or raise the minimum version of the extension.

Using exceptions from controllers, helpers and plugins

Exceptions need a catcher. A controller task catches them, reports through the message queue and redirects; a helper lets them propagate; a plugin event handler must be careful, because an uncaught exception in one plugin aborts the dispatch for everyone:

<?php

namespace Tallpine\Component\Tallpine\Administrator\Controller;

use Joomla\CMS\MVC\Controller\FormController;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;

\defined('_JEXEC') or die;

class TrailController extends FormController
{
    public function publishWithConditions(): bool
    {
        $this->checkToken();

        $id    = $this->input->getInt('id');
        $model = $this->getModel('Trail');
        $model->setUseExceptions(true);
        $ok    = false;

        try {
            $model->publishWithConditions($id);
            $this->setMessage(Text::_('COM_TALLPINE_TRAIL_PUBLISHED'));
            $ok = true;
        } catch (\Throwable $e) {
            // Show a translated, generic text to the user; the details go to the log.
            \Joomla\CMS\Log\Log::add($e->getMessage(), \Joomla\CMS\Log\Log::ERROR, 'com_tallpine');
            $this->setMessage(Text::_('COM_TALLPINE_ERROR_PUBLISH_FAILED'), 'error');
        }

        $this->setRedirect(Route::_('index.php?option=com_tallpine&view=trails', false));

        return $ok;
    }
}

The same code shows two security points. First, the exception message is not echoed to the user: it may contain SQL fragments, file paths or table names. Second, checkToken() runs before any state change. Joomla’s own error pages follow the Global Configuration setting Error Reporting (System Default, None, Simple or Maximum); see Error, Offline and Component Pages and Debugging for what each level reveals, and Security in Extensions for the broader checklist.

Pitfalls

  • Checking the return value only. A method that used to return false may now throw, and the caller’s if (!$table->store()) never runs the error branch. Wrap the call, or catch at a higher level.

  • Catching \Exception and swallowing it. Joomla’s PHP code runs under \Throwable semantics: PHP 8 raises \Error subclasses (TypeError, ValueError) that catch (\Exception $e) does not catch. Catch \Throwable only at the outer boundary where you log and recover.

  • setError() with an exception object. With the flag on, only strings throw. Passing an \Exception object records it silently.

  • Calling getError() after enabling exceptions. There is nothing to read: the array stays empty.

  • Relying on getErrors() in a layout. Views in 6.x have exceptions turned on by core; error output moves to the exception handler.

  • Putting your own plugin in the behaviour group to guarantee early execution. The Manual warns that the group may be removed; use the system group with an explicit priority.

The Behaviour - Backward Compatibility 6 plugin

Most legacy APIs removed in 6.0 are not simply gone: the plugin Behaviour - Backward Compatibility 6 (folder plugins/behaviour/compat6, abbreviated BC6) ships the old classes and aliases, so that an extension written for 5.x keeps working until it is updated. The plugin is a final class that extends CMSPlugin and implements SubscriberInterface; it is in the behaviour group so that it loads before any other plugin.

Its behaviour differs by how a site got to 6.x:

  • On a site updated from 5.4, the plugin has already been installed and enabled in 5.4 (where it does nothing), and the updater requires it to be enabled. The old Behaviour - Backward Compatibility plugin of Joomla 5 (the one that bridged 4 to 5) must be disabled before the update; if the site breaks without it in 5.4, enable it again and fix the cause while still on 5.4.

  • On a new Joomla 6 installation the plugin is installed but disabled.

Its parameters, as defined in compat6.xml, are three switches:

Parameter Default What it does

Classes Aliases (classes_aliases)

No

Loads classmap.php, which registers the legacy J* aliases (JFactory, JText, JHtml, JModelAdmin, JApplicationWeb and so on, about 450 entries) with JLoader::registerAlias().

Include Deprecated Classes (legacy_classes)

Yes

Registers the folder plugins/behaviour/compat6/classes as the source of the \Joomla\CMS namespace for classes that were removed from the core: \Joomla\CMS\Input\*, \Joomla\CMS\Filesystem\* and the CMS application base classes.

Removed Assets (removed_asset)

Yes

Registers removed.asset.json, which defines empty stubs for assets that existed in Joomla 5 and are gone, so that code asking for them does not throw an exception.

The plugin constructor also defines JPATH_PLATFORM (with a deprecation note) so that old class files, which begin with defined('JPATH_PLATFORM') or die, can still be loaded. Section JPATH_PLATFORM and _JEXEC explains why that matters.

Checking an extension against the plugin

The practical question is "does pkg_tallpine work with BC6 disabled?", because BC6 is itself transitional: its classes are announced for removal in 7.0. A reliable procedure on a development site:

  1. Install the extension on a fresh Joomla 6.2 site where BC6 is disabled (the default for new installs).

  2. Open every administrator view and every site view, save an item, run the installer, and read the PHP error log with Error Reporting set to Maximum.

  3. A Class "JFactory" not found message, or a similar one for Joomla\CMS\Input\Input, means the code still depends on an alias or a removed class. Fix the code rather than enabling the plugin.

  4. Enable BC6 only temporarily to find out which of its three switches the extension needs, and write that down as a to-do list.

The plugin is a rescue tool, not a platform. A site that turns on Classes Aliases permanently has simply postponed the work to 7.0, and an extension that requires BC6 should say so in its description and compatibility notes.

Legacy J* class aliases

Joomla 1.5 to 3.x named every class with a J prefix and a flat name: JFactory, JText, JHtml, JRoute, JUri, JTable, JModelLegacy, JControllerLegacy, JPlugin. Joomla 4 moved everything to namespaces under \Joomla\CMS and kept the old names as aliases registered with JLoader::registerAlias($alias, $original, $version); the third argument is the version in which the alias is to be removed, and using an alias logs a deprecation.

In Joomla 6.0 the aliases are no longer registered by core. They are available only when BC6 is enabled and its Classes Aliases switch is on, and that switch is off by default. The replacement is always the namespaced class, with an use statement. A before-and-after for a helper of com_tallpine:

<?php
// Before (Joomla 3 and 4 style; fails on a default Joomla 6 site)
defined('_JEXEC') or die;

JLoader::register('TallpineHelper', JPATH_COMPONENT . '/helpers/tallpine.php');

class TallpineHelper
{
    public static function nextHikeTitle(): string
    {
        $app = JFactory::getApplication();
        $db  = JFactory::getDbo();

        return JText::_('COM_TALLPINE_NEXT_HIKE') . ' ' . $app->input->get('trail');
    }
}
<?php
// After (Joomla 6)
namespace Tallpine\Component\Tallpine\Site\Helper;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;

\defined('_JEXEC') or die;

class TallpineHelper implements DatabaseAwareInterface
{
    use DatabaseAwareTrait;

    public function nextHikeTitle(): string
    {
        $app = Factory::getApplication();

        return Text::_('COM_TALLPINE_NEXT_HIKE') . ' ' . $app->getInput()->getString('trail');
    }
}

Notes on this migration:

  • JFactory::getDbo() and the static Factory::getDbo() are not the target. Inject the database through DatabaseAwareTrait or take it from the dependency-injection container; see Database API and Namespaces, Autoloading and Dependency Injection.

  • Factory::getApplication() itself is still available in 6.2, but inside classes created by Joomla’s factories prefer getApplication() from CMSApplicationAwareTrait. Plugins and the application classes expose getApplication() as well; the plugin properties $app and $db that older code reads are auto-detected today and that detection is removed in 7.0.

  • $app->input is deprecated too: since the framework application dropped the property, the CMS keeps a magic accessor that logs a deprecation. Use $app->getInput().

  • Table access changed in 6.0: core uses new \Joomla\CMS\Table\Content($db) instead of Table::getInstance('content'), and Table::addIncludePath() can no longer be used to substitute a core table class. An extension of your own should construct its table class directly or use the MVC factory.

  • The non-namespaced indexer files under administrator/components/com_finder/helpers/indexer are empty; FinderIndexerHelper::getFinderPluginId() becomes Joomla\Component\Finder\Administrator\Helper\FinderHelper::getFinderPluginId().

A quick way to find what is left in your own repository is a search for the two-letter prefix; this is a shell one-liner, not a Joomla tool:

grep -rnE "\bJ(Factory|Text|Html|Route|Uri|Table|Plugin|Loader|Log|Input|Filter[A-Za-z]*|Model[A-Za-z]*|Controller[A-Za-z]*|View[A-Za-z]*)::?|new J[A-Z]|extends J[A-Z]" \
  --include='*.php' src/ pkg_tallpine/

JPATH_PLATFORM and _JEXEC

Every PHP file that Joomla can load should refuse direct execution, so that requesting a file such as /administrator/components/com_tallpine/src/Table/TrailTable.php over HTTP does not run it outside the CMS. Joomla uses a constant defined by the entry point. Two such constants existed:

  • _JEXEC is defined by the entry points (index.php of the site, administrator and API, and the command line). It means "this request goes through Joomla". The extension guard is \defined('_JEXEC') or die;.

  • JPATH_PLATFORM was defined by the old platform bootstrap and was the guard for the library classes. It was deprecated in 4.4 and, in Joomla 6.0, is no longer defined by any bootstrap file. It exists only while BC6 is enabled, because the plugin defines it for the sake of old class files.

Consequence: a file that starts with defined('JPATH_PLATFORM') or die; stops with a blank page on a default Joomla 6.x site, because the constant is undefined and die runs. The fix is mechanical:

<?php
// Old:
\defined('JPATH_PLATFORM') or die;

// New:
\defined('_JEXEC') or die;

Details worth knowing:

  • Use the fully qualified \defined (leading backslash) inside a namespace, otherwise PHP first looks for a function defined in the namespace; core files use \defined('_JEXEC') or die; on its own line, between the namespace line and the code.

  • The guard is a defence-in-depth measure and not authentication. Files with only declarations (classes, interfaces, traits) are harmless when requested directly, but the guard prevents notices that would reveal paths and avoids running procedural code by accident. Procedural files, layouts and services/provider.php must always have it.

  • Do not replace die with die('message') that prints environment details.

  • The coding standard and PHPCS ruleset of the project already require this form. See Coding Standards and Static Analysis.

\Joomla\CMS\Input becomes \Joomla\Input

Request data is read from an input object: $input->get('id'), $input->getInt(), $input->post, $input->files, $input->cookie, $input->json. In Joomla 3 and 4 the class was \Joomla\CMS\Input\Input (with the subclasses Cookie, Files and Json), an extension of the Joomla Framework class. It was deprecated in 4.3 and, in Joomla 6.0, the CMS namespace was removed from the core and moved into the BC6 plugin. All MVC classes (controllers, models, views, the dispatcher) now hand out the Framework class \Joomla\Input\Input from the package joomla/input, which is described by the Manual as "very much a drop-in replacement".

For an extension this affects imports and type declarations, not call sites:

<?php

namespace Tallpine\Component\Tallpine\Site\Helper;

// Old, removed from core in 6.0 (available only with BC6):
// use Joomla\CMS\Input\Input;

use Joomla\Input\Input;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

class TrailFilter
{
    public static function fromRequest(?Input $input = null): array
    {
        $input ??= Factory::getApplication()->getInput();

        return [
            'difficulty' => $input->getCmd('difficulty', ''),
            'max_km'     => $input->getFloat('max_km', 0.0),
            'family'     => $input->getBool('family', false),
        ];
    }
}

A type declaration against the old class is the common failure: a method declared with \Joomla\CMS\Input\Input $input gets a \Joomla\Input\Input from the core and throws a TypeError. An extension that must run on both 5.x and 6.x can drop the type or type against \Joomla\Input\Input, which exists in both (the old class extended it).

Security-wise nothing changed in the filtering rules: always read through a typed getter (getInt, getCmd, getBool, getString, getFloat, getWord), never $_GET or $_POST, and use Joomla\CMS\Filter\InputFilter for the cases where filtering must differ. Uploaded files come from $input->files and, in Joomla 6, are handled with the Framework filesystem package described next.

\Joomla\CMS\Filesystem becomes joomla/filesystem

The CMS filesystem helpers (File, Folder, Path, Stream, FilesystemHelper, Patcher) were deprecated since 4.4 in favour of the Joomla Framework package joomla/filesystem with the namespace \Joomla\Filesystem. In Joomla 6.0 they were moved to the BC6 plugin, and they are scheduled to disappear completely in 7.0.

The Framework package ships with the core, so no Composer step is needed for an extension that runs inside Joomla. A standalone library (for tests or a CLI tool) declares the dependency:

{
    "name": "tallpine/pkg-tallpine",
    "require": {
        "php": ">=8.3",
        "joomla/filesystem": "^4.2.1",
        "joomla/input": "^4.0"
    }
}

Joomla 6.2.1 itself requires joomla/filesystem 4.2.1 and joomla/input 4.0. The Manual says the two APIs "can be used nearly interchangeably", with one exception that causes the most surprises: it states that File::exists() and Folder::exists() are not available in the Framework package and recommends is_file() and is_dir(). The 4.2.1 Framework source does contain both methods as thin wrappers, but older Framework releases do not, so the built-ins are the portable choice:

<?php

namespace Tallpine\Component\Tallpine\Administrator\Helper;

use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
use Joomla\Filesystem\Path;

\defined('_JEXEC') or die;

class GpxStorage
{
    public static function store(string $tmpName, string $fileName): string
    {
        $dir = JPATH_ROOT . '/images/tallpine/gpx';

        // Old: if (!Folder::exists($dir)) { Folder::create($dir); }
        if (!is_dir($dir)) {
            Folder::create($dir);
        }

        // Never trust the original name: keep only a safe file name and check the final path.
        $safe   = File::makeSafe($fileName);
        $target = Path::clean($dir . '/' . $safe);

        Path::check($target); // throws if the path points outside the Joomla root

        // Old: if (File::exists($target)) { ... }
        if (is_file($target)) {
            throw new \RuntimeException('COM_TALLPINE_ERROR_GPX_EXISTS');
        }

        File::upload($tmpName, $target);

        return $target;
    }
}

Two behavioural points matter for the error-handling topic of this page:

  • The Framework classes throw exceptions (\Joomla\Filesystem\Exception\FilesystemException; for example from File::upload(), File::copy(), Folder::create() and Path::check()) instead of returning false and queueing a warning, which was the habit of the CMS classes. Code that wrote if (!File::upload($src, $dest)) { ... } must switch to try/catch. File::upload() also refuses unsafe files (isSafeFile()) unless $allowUnsafe is set; never set it for user uploads.

  • Path::check() and File::makeSafe() are security tools. Path traversal (../) in an uploaded file name is one of the classic weaknesses of extensions; see Security in Extensions.

The sibling classes follow the same pattern. The CMS \Joomla\CMS\Application\BaseApplication and CliApplication and the CLI input and output classes are replaced by the namespace \Joomla\Application and by console plugins (see Console Command Plugins); \Joomla\CMS\Adapter\Adapter and AdapterInstance were removed without replacement, and $app->isCli() is replaced by an instanceof test against \Joomla\CMS\Application\ConsoleApplication. In the 5.4 to 6.0 notes, \Joomla\CMS\Http is additionally deprecated (removal in 8.0) in favour of the Framework \Joomla\Http, whose responses must be read through the PSR-7 methods rather than magic attributes.

CMSObject removal

\Joomla\CMS\Object\CMSObject (formerly JObject, Joomla 1.5/1.7) was a base class offering get(), set(), setProperties(), getProperties(), def(), getPublicProperties(), error handling and toString(). It also allowed code to read and write properties regardless of their visibility, which is the reason the maintainers wanted it gone. It has been deprecated since 4.0 and is removed in 7.0. Its two jobs now live in LegacyErrorHandlingTrait and LegacyPropertyManagementTrait, which are also deprecated.

Joomla 6.0 removed every use of it from the core code, which created these changes that are not backward compatible:

  • AdminModel::getItem() returns a \stdClass, not a CMSObject. The method calls $item->get('title') and $item->set('title', 'x') no longer exist; read and write the property directly.

  • The models listed by the Manual now return \stdClass: those of the Installer (update sites), Languages, Mails (template), Menu, Menus (menu types), Messages (config), Modules, Plugins, Scheduler (task and tasks), Templates (style), Users (group), Workflow (transition), the FormModel of Contact and Content, and the Tags TagModel.

  • Smart Search plugins keep their state in \stdClass objects, Rules::getAllowed() returns a stdClass, UserHelper::getProfile() returns a stdClass, the model state of ApiController is a Registry, and the events fired when a media file is uploaded or a folder is created now carry a stdClass.

  • Views lost the $this->db property. A Joomla 3 era override that compares $this->item->created to $this->db->getNullDate() should compare to null; since Joomla 4 the database stores real NULL values instead of 0000-00-00 00:00:00.

<?php
// Before (CMSObject API on a model item)
$trail = $this->getModel('Trail')->getItem($id);
$km    = $trail->get('distance_km', 0);

if ($trail->created !== $this->db->getNullDate()) {
    echo $trail->created;
}

// After (Joomla 6)
$trail = $this->getModel('Trail')->getItem($id);
$km    = $trail->distance_km ?? 0;

if ($trail->created !== null) {
    echo $trail->created;
}

For your own data classes, replace a CMSObject base with a plain class with typed, visibility-controlled properties, or with \stdClass or a Joomla\Registry\Registry where a bag of values is really needed. Do not use CMSObject as the return type of a new method: the class does not exist in 7.0.

UTC instead of GMT

Joomla stores all dates in the database in UTC and converts them to the user’s or site’s time zone for display. For historical reasons some code used the string GMT for "no offset". Joomla 6.0 unified this: all places use or fall back to UTC, and the Date class forces PHP’s default time zone to UTC while it works. Two related removals belong to the same change: \Joomla\CMS\Date\Date::$gmt and Date::$stz and the code around them are gone, so a class extending Date must not depend on them.

<?php

use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;

\defined('_JEXEC') or die;

$tz = Factory::getApplication()->getIdentity()->getTimezone();

// Old assumption: "if ($tz->getName() === 'GMT')" to detect "no offset configured".
// New: compare with UTC, or better, do not compare names at all.
$isUtc = $tz->getName() === 'UTC';

$departure = new Date($trail->departure_at, 'UTC');   // value as stored in the database
echo $departure->setTimezone($tz)->format('Y-m-d H:i'); // value shown to the visitor

Pitfalls: a template override or plugin that tests for the literal string GMT, a database default written as GMT in a SQL script, or a unit test that asserts GMT in a formatted date will all fail after the move. Fix the comparison; do not set the server to GMT to compensate. See Global Configuration for the Website Time Zone setting.

Other 6.0 removals met in extension code

The Manual’s list is long; these items regularly appear in extensions and are best handled together with the ones above:

Removed or changed in 6.0 What to use

Table::getInstance('content') and Table::addIncludePath() overriding core tables in core code

Construct the table class directly: new \Joomla\CMS\Table\Content($db).

$app variable in plugins/*/tmpl layouts

$this->getApplication() in the plugin class; layouts receive what they need as data.

Image::createThumbs()

Image::createThumbnails().

ActionlogsHelper::getLogContentTypeParams()

The ActionlogConfig administrator model’s method of the same name.

UpdateController::fetchExtensionCompatibility() in com_installer

batchextensioncompatibility().

BufferStreamHandler registering itself

Call BufferStreamHandler::stream_register() yourself.

mod_breadcrumbs setSeparator(), tfa login property, JCOMPAT_UNICODE_PROPERTIES, $item_associations, buildVotingQuery()

Removed without replacement; copy the logic from the 5.3 branch if you really need it.

voku/portable-utf8, TYPO3/phar-stream-wrapper Composer packages

Removed from the core. Use joomla/string or PHP’s mb_* functions.

cleanCache($clientId) signature in form models

Drop the $clientId argument in overrides.

A few of the entries in the Manual’s chapter are still marked as needing more detail (for example the replacement for the extension compatibility fetch and the extended #__ucm_history data); check the chapter again before relying on a particular replacement.

Where older books and tutorials are outdated

  • Developing Extensions for Joomla! 5 (Packt, 2023) was written for Joomla 5.0. Code in the book that reads $model->getError() after a false return, throws GenericDataException from getErrors(), imports Joomla\CMS\Input\Input, or uses Joomla\CMS\Filesystem\File runs on 5.x, but on Joomla 6 it needs BC6 or an update. The book predates setUseExceptions() (5.4) and the exception-first guidance.

  • Joomla! 4 Masterclass (Packt, 2023) targets Joomla 4.x, which is end of life, and treats JFactory-style code and $this->input as normal; none of that is current on 6.2.

  • Tutorials on docs.joomla.org (the legacy wiki) still show JError::raiseWarning(), JRequest::getVar() and defined('JPATH_PLATFORM') or die. Those calls belong to the Joomla 1.5 to 3.x era and are long gone; the others are covered above. Joomla’s documentation moved to the Developer Manual (manual.joomla.org) and the User Guide (guide.joomla.org).

Migration checklist for pkg_tallpine

  1. On Joomla 5.4.x, set Error Reporting to Maximum on a development copy, so deprecation notices show up, and run the extension through all views.

  2. Replace setError()/getError() with exceptions; add setUseExceptions(true) in every class that extends a core model, table or view base class (guarded with method_exists if you still support 5.3 or older).

  3. Replace defined('JPATH_PLATFORM') with \defined('_JEXEC').

  4. Replace J* aliases with namespaced classes and use statements; replace $app->input with $app->getInput().

  5. Replace \Joomla\CMS\Input and \Joomla\CMS\Filesystem imports with \Joomla\Input and \Joomla\Filesystem, and replace File::exists() and Folder::exists() with is_file() and is_dir().

  6. Replace CMSObject use with plain objects and property access; remove $this->db from view layouts.

  7. Replace comparisons with GMT by UTC.

  8. Install the extension on a fresh 6.2 site with BC6 disabled and repeat the test.

  9. State the supported Joomla versions in the manifest <targetplatform> and in the update server file, see Extension Types, Manifests and Packaging.