Prepare for the Library Upgrades coming in 12.3 (Q1 2023)

Preparing for the Library Upgrades coming on 12.3 (Q1 2023)

Sugar utilizes multiple PHP libraries in its core that will undergo major version upgrades in the 12.3 (Q1 2023) release. Some of those libraries have significant and potentially breaking changes.

Note that 12.3 also includes the new UI Redesign, therefore, you must plan accordingly to incorporate the new UI changes as well as these library upgrades in your customizations.

In this article, we will highlight a subset of libraries upgraded in this release and how you can prepare/plan for them. These are the most impactful major version library upgrades in 12.3.

Library

From

To

doctrine/dbal

^2.13.2

3.3.7

monolog/monolog

1.22.0

^2.8.0

smarty/smarty

3.1.45

^4.2.0

moontoast/math

1.1.0

brick/math@0.9.3

guzzlehttp/guzzle

6.3.3

^6.5.8

You will notice in some versions Sugar has added the prefix character caret (“^”). It allows Composer to resolve to the latest minor version of the package, you can check more details about it in this blog post. Basically, minor version upgrades to these libraries that do not introduce backward compatibility issues are possible in subsequent Sugar releases.

Who is affected by this change?

For most customers, we expect this change to be seamless and without a noticeable impact. However, if you have customized Sugar and used low-level 3rd party classes in the vendor library then you will be affected by these upgrades. SugarCloud customers will be impacted first with the 12.3 upgrades. On-premise customers should expect the upgrade in Sugar 13.0 based on Sugar's typical release cadence

What will happen during the upgrade?

The Sugar upgrader will examine your instance’s filesystem looking for incompatible code. The upgrader will identify known incompatibilities with Doctrine DBAL and Monolog. For other minor upgrades, SugarCRM doesn’t know of any issues therefore no actions will be taken by the upgrader.

Smarty on the other hand has gone through major upgrades that introduce breaking changes. Sugar is working on a solution to minimize the impact, and our final recommendation will be posted in our customization guide. For now, we suggest you prepare and plan for this change.

What action do I need to take? 

At the very least, you should plan to test the customizations that exist in your Sugar instance and ensure they still work as expected after the upgrade.

If you've built an app or integration for Sugar that uses a Module Loadable Package (MLP) that includes code related to:

Then you will need to update your code manually to use the new library version’s syntax. You will also need to update your module manifest to indicate compatibility with Sugar 12.3+. 

To proactively identify and resolve library upgrade issues prior to GA, we encourage you to participate in the upcoming Sugar Release Previews for Sugar 12.3 (SugarCloud release) and Sugar 13.0 (on-premise release). If you are not a member of the Sugar Release Preview program and would like to be added, get in touch with developers@sugarcrm.com.

Most changes cannot be verified until you are running Sugar 12.3. But depending on your particular customizations, you may find some things that you can fix right now or plan to test when the release preview is out. Continue reading below to see if you think you can get a head start on any of the changes we are highlighting.

Significant breaking changes Doctrine DBAL 2.x and 3.x

The official library upgrade notes contain all the breaking changes between releases.

The Doctrine\DBAL\DBALException class has been renamed to Doctrine\DBAL\Exception.

Replace

With

$id = '1234-abcde-fgh45-6789';
$query = 'SELECT * FROM accounts WHERE id = :id';
$conn = $GLOBALS['db']->getConnection();
// before
try {
  $result = $conn->executeQuery($query, ['id' => $id]);
} catch (\Doctrine\DBAL\DBALException $e) {
  // ...
}

$id = '1234-abcde-fgh45-6789';
$query = 'SELECT * FROM accounts WHERE id = :id';
$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->executeQuery($query, ['id' => $id]);
try {
  $result = $conn->executeQuery($query, ['id' => $id]);
} catch (\Doctrine\DBAL\Exception $e) {
  // ...
}

The usage of the colon prefix when binding named parameters is no longer supported.

Replace

With

$id = '1234-abcde-fgh45-6789';
$query = 'SELECT * FROM accounts WHERE id = :id';
$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->executeQuery($query, [':id' => $id]);

$id = '1234-abcde-fgh45-6789';
$query = 'SELECT * FROM accounts WHERE id = :id';
$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->executeQuery($query, ['id' => $id]);

Dropped handling of one-based numeric arrays of parameters in Statement::execute()

The statement implementations no longer detect whether $params is a zero- or one-based array. A zero-based numeric array is expected.

Replace

With

$id = '1234-abcde-fgh45-6789';

$query = 'SELECT * FROM accounts WHERE id = ?';

$conn = $GLOBALS['db']->getConnection();

// before

$stmt = $conn->prepare($query);

$result = $stmt->execute([1 => $id]);

$id = '1234-abcde-fgh45-6789';

$query = 'SELECT * FROM accounts WHERE id = ?';

$conn = $GLOBALS['db']->getConnection();

// after

$stmt = $conn->prepare($query);

$result = $stmt->executeQuery([$id]);

Removed FetchMode and the corresponding methods

  • The FetchMode class and the setFetchMode() method of the Connection and Statement interfaces are removed.
  • The Statement::fetch() method is replaced with Result::fetchNumeric(), Result::fetchAssociative() and Result::fetchOne(). 
  • The Statement::fetchColumn() method is replaced with fetchOne() when fetching the first column or more generally with fetchNumeric() to get the required column by number from its result

Replace

With

$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->executeQuery("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetch(FetchMode::NUMERIC);
$result = $conn->executeQuery("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetch(FetchMode::ASSOCIATIVE);
$result = $conn->executeQuery("SELECT name FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetch(FetchMode::COLUMN);

$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->executeQuery("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetchNumeric();
$result = $conn->executeQuery("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetchAssociative();
$result = $conn->executeQuery("SELECT name FROM accounts WHERE id='1234-abcde-fgh45-6789'")->fetchOne();

The Statement::fetchAll() method is replaced with Result::fetchAllNumeric(), Result::fetchAllAssociative() and Result::fetchFirstColumn(). 

Replace

With

$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->executeQuery("SELECT * FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchAll(FetchMode::NUMERIC);
$result = $conn->executeQuery("SELECT * FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchAll(FetchMode::ASSOCIATIVE);
$result = $conn->executeQuery("SELECT name FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchAll(FetchMode::COLUMN);
$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->executeQuery("SELECT * FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchAllNumeric();
$result = $conn->executeQuery("SELECT * FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchAllAssociative();
$result = $conn->executeQuery("SELECT name FROM accounts WHERE parent_id='1234-abcde-fgh45-6789'")->fetchFirstColumn();

The Connection::fetchArray() and fetchAssoc() methods are replaced with fetchNumeric() and fetchAssociative() respectively.

Replace

With

$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->fetchArray("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'");
$result = $conn->fetchAssoc("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'");

$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->fetchNumeric("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'");
$result = $conn->fetchAssociative("SELECT * FROM accounts WHERE id='1234-abcde-fgh45-6789'");
 

The functionality previously available via Statement::closeCursor() is now available via Result::free().

Replace

With

$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->fetchAll();
$result->closeCursor();

$conn = $GLOBALS['db']->getConnection();
// before
$result = $conn->fetchAllAssociative();
$result->free();
 

The StatementIterator class is removed. The usage of a Statement object as Traversable is no longer possible. Use iterateNumeric(), iterateAssociative() and iterateColumn() instead.

Replace

With

$conn = $GLOBALS['db']->getConnection();
// before
$stmt = $conn->executeQuery("SELECT * FROM accounts");
foreach ($stmt as $row) {
   $name = $row['name'];
   // do something
}

$conn = $GLOBALS['db']->getConnection();
// after
$result = $conn->executeQuery("SELECT * FROM accounts");
foreach ($result->iterateAssociative() as $row) {
    $name = $row['name'];
    // do something
}

Note: Fetching data in mixed mode (former FetchMode::MIXED) is no longer possible. 

Significant breaking changes Monolog

The official library upgrade notes contain all the breaking changes between releases. If you have used Monolog Handlers in your customizations, pay attention to those:

  • In Monolog v2 some class methods have changed signatures (and related Mango methods too), namely, they have declared return type - this breaks inheritance. Please review the customizations code and if you extend classes or implement interfaces from Monolog, make sure that your methods have compatible signatures, usually it means adding a return type.

Replace

With

// Monolog LineFormatter method, before
public function format(array $record)

// Monolog LineFormatter method, after
public function format(array $record): string

// Custom formatter - return type is wider than parent, it causes PHP fatal error
public function format(array $record)

// Custom formatter fixed - compatible with both Monolog v1 and v2
public function format(array $record): string

List of affected Classes:

//All classes in Monolog\Handler and Monolog\Processor namespaces
Sugarcrm\Sugarcrm\Logger\Formatter
Sugarcrm\Sugarcrm\Logger\Formatter\BackwardCompatibleFormatter
Monolog\Formatter\FlowdockFormatter
Monolog\Formatter\FluentdFormatter
Monolog\Formatter\GelfMessageFormatter
Monolog\Formatter\HtmlFormatter
Monolog\Formatter\JsonFormatter
Monolog\Formatter\LineFormatter
Monolog\Formatter\LogglyFormatter
Monolog\Formatter\LogstashFormatter
Monolog\Formatter\MongoDBFormatter
Monolog\Formatter\ScalarFormatter

  • HandlerInterface now requires the close method to be implemented. This only impacts you if you implement the interface yourself, but you can extend the new Monolog\Handler\Handler base class too.
  • Some handlers have been renamed/removed/reconfigured, see upgrade notes if you instantiate or inherit them (LogglyFormatter, AmqpHandler, RotatingFileHandler, LogstashFormatter, HipChatHandler, SlackbotHandler, RavenHandler, ElasticSearchHandler)
  • Removed non-PSR-3 methods to add records, all the add* (e.g. addWarning) methods as well as emerg, crit, err and warn. It may affect those customers, who use PSR-3 compatible logger in Sugar, but call Monolog-specific methods like addWarning instead of standard methods, listed in Sugar documentation (seems to be a very uncommon case)

Replace

With

use \Sugarcrm\Sugarcrm\Logger\Factory;
$logger = Factory::getLogger('default');

// no longer supported
$logger->addDebug('something happened');
$logger->addInfo('something happened');
$logger->addNotice('something happened');
$logger->addWarning('something happened');
$logger->addError('something happened');
$logger->addCritical('something happened');
$logger->addAlert('something happened');
$logger->addEmergency('something happened');
$logger->warn('something happened');
$logger->err('something happened');
$logger->crit('something happened');
$logger->emerg('something happened');

use \Sugarcrm\Sugarcrm\Logger\Factory;
$logger = Factory::getLogger('default');

// replace with these
$logger->debug('something happened');
$logger->info('something happened');
$logger->notice('something happened');
$logger->warning('something happened');
$logger->error('something happened');
$logger->critical('something happened');
$logger->alert('something happened');
$logger->emergency('something happened');