After upgrading your Magento 2 installation to version 2.4.4 or above you may encounter lots of fatal errors. These are mostly regarding deprecated behavior that occur at run time. The issue is just because of the PHP upgrade to version 8. PHP 8 has become more strict regarding the data types in its core.
So the old code that was working before Magento 2.4.4 will no more work. Since starting from PHP 8 all pure functions now require strictly typed parameters. Passing anything else may fail if that could not be automatically converted to the required type.
Examples of deprecated functionality in PHP 8
Below are some examples of usage behaviors that are now marked as deprecated. Why usage behavior? Because the functions themselves are not outdated. Only the way we are using it has changed. Now the parameters to the functions are strictly typed. They are throwing an ERROR exception if the required type is mismatched with the actual type of the parameter passed or null is passed for the parameter at runtime.
- number_format(): Passing null to parameter #1 ($num) of type float is deprecated
- Automatic conversion of false to array is deprecated
- utf8_decode(): Passing null to parameter #1 ($string) of type string is deprecated
- explode(): Passing null to parameter #2 ($string) of type string is deprecated
- Undefined variable $something
- strtoupper(): Passing null to parameter #1 ($string) of type string is deprecated
- preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated
- base64_decode(): Passing null to parameter #1 ($string) of type string is deprecated
- Optional parameter $data declared before required parameter $user_agent is implicitly treated as a required parameter
- nl2br(): Passing null to parameter #1 ($string) of type string is deprecated
- strlen(): Passing null to parameter #1 ($string) of type string is deprecated
- Array like access of an object that does not explicitly extends ArrayAccess now throws ERROR exception
- count(): passing stdClass to parameter #1 of type array or array like object is deprecated
The Problem With Existing Code
Starting from PHP 8 lots of strong decisions have been made at the core level. Now E_ALL is the error reporting behavior without any exclusion. And in most cases, PHP now throws an ERROR exception. See this article on “Backward Incompatible Changes“. Yes, you got it right. It is backward incompatible and hence we have to change our code.
The solution to this problem
There is no bypass to this problem as it is backward incompatible. We have to make our code corrections with strict typing implementation in our code base and make sure there is no chance of null or a different type of parameter being passed at run time. Let’s see how to do that in the bellow example code.
<?php
//error_reporting(E_ALL);
//ini_set('display_errors', 1);
//echo phpinfo(); // php8
function encodeMyString($str)
{
echo utf8_encode($str);
}
$myStringToBeEncoded = 'hello';
encodeMyString($myStringToBeEncoded); // okay
$myStringToBeEncoded = null;
encodeMyString($myStringToBeEncoded); // deprecated error
// Solution #1 [ place an inline check while calling the function ]
encodeMyString($myStringToBeEncoded ?? ''); // okay
// solution #2 [ change your own wrapping function signature to be more strict like PHP8 ]
function encodeMyString(string $str): void
{
echo utf8_encode($str);
}
Here in the above code block we are passing null to the function call and the function will throw the deprecated error. This null could be evaluated from various different sources in your code as mentioned bellow.
- may be a form input
- may be another method call response
- may be API call response
- may be a variable assignment
- may be evaluated expression
- may be a function parameter
Knowingly or unknowingly we are somehow making the variable null and passing it to the function. So we have to check all those possibilities to stop happening this.
Solution #1: Place an inline check while calling the function
You can place an inline check in your code while calling such deprecated function. If you are calling such PHP core pure functions openly like in procedural way then you have to must follow this solution. See the solution implementation in the code block above.
Solution #2: Change your own wrapping function signature to be more strict like PHP8
This is the best way to code in latest PHP8 and you should follow best practices. See the solution implementation in the code block above.
More Real Life Examples
Example With Inline Data Type Check
In the following example the data is coming as form input. The input fields in the following case are all of type text. We don’t want to store an empty string (”) at any cost in the database. But if user did not fill address line #2 and #3 as those are not required, there is a chance that those fields could be empty string. Hence, we are passing default null to request get post method call.
The response of the request get post call is finally becoming the input for utf8 decode function. And it’s clear that there is a chance of passing null to utf8 decode function. But utf8 decode function expects a parameter of type string. So the old code is not backward compatible anymore in PHP8. See the solution in the code block below, the new implementation for PHP8.
/** OLD Code for PHP7 */
private function composeStreetArrayFromPostData($request)
{
$street = [];
$street[] = utf8_decode($request->getPost('address1', null));
$street[] = utf8_decode($request->getPost('address2', null));
$street[] = utf8_decode($request->getPost('address3', null));
return $street;
}
/** NEW Code for PHP8 With Type Safe Check */
private function composeStreetArrayFromPostData(RequestInterface $request): array
{
$street = [];
$street[] = $request->getPost('address1', null) ? utf8_decode($request->getPost('address1')) : null;
$street[] = $request->getPost('address2', null) ? utf8_decode($request->getPost('address2')) : null;
$street[] = $request->getPost('address3', null) ? utf8_decode($request->getPost('address3')) : null;
return $street;
}
One more example where the cause of the error is an input from database entry. In case that is not configured in database it will return null. As a result we will get deprecated error while using the database result in some PHP core function.
The scope config get value call for a config string could return null if that config is missing or not set at all.
/** OLD Code for PHP7 */
private function sendMail()
{
...
...
$receiverEmailIdsString = $this->scopeConfig->getValue('some/config/string', ScopeInterface::SCOPE_STORE);
$receiverEmailIds = explode(',', $receiverEmailIdsString); // can throw deprecated error
...
}
/** NEW Code for PHP8 */
private function sendMail()
{
...
...
$receiverEmailIdsString = (string) $this->scopeConfig->getValue('some/config/string', ScopeInterface::SCOPE_STORE);
$receiverEmailIds = explode(',', $receiverEmailIdsString);
...
}
Example Where We should Change the Function Signature
The below code is an observer that gets triggered on each cart load.
/**
* Adjust delivery date.
* If customer did not place order for the current cart in time.
* Default delivery date is the suggetded system generated delivery date proposed to customer on the cart
* page. Expected delivery date is something that customer wants his/her items to be delivered on. Each item
* in the cart has a delivery date associated with it.
* If customer delayed for days to place the order and thus the expected/default delivery date of any item in
* the cart passes beyond the current date, then we have to update the delivery date as per the current date.
*
*
* @param string $productInfo
* <pre>
* $productInfo = [
* ...
* 'default_delivery_date', // system generated delivery date
* 'expected_delivery_date' // customer want delivery on this date
* ]
* </pre>
*/
private function adjustDeliveryDate(&$productInfo)
{
$deliveryAfter = $productInfo['delivery_after'];
$currentDate = new DateTime();
$calculatedDefaultDeliveryDate = date_add($currentDate, DateInterval::createFromDateString($deliveryAfter . ' day'));
// expected delivery date is not configured by customer, we will use default calculated delivery date
if (!array_key_exists('expected_delivery_date', $productInfo)) {
// comparing last calculated default delivery date with current date
if (strtotime($productInfo['default_delivery_date']) < strtotime($currentDate->format('Y-m-d'))) {
// will use newly calculated default delivery date
$productInfo['default_delivery_date'] = $calculatedDefaultDeliveryDate;
$productInfo['delivery_date'] = $calculatedDefaultDeliveryDate;
}
} else {
// we can not full fill the expected delivery date as it is not allowed by the product
if (strtotime($productInfo['expected_delivery_date']) < strtotime($calculatedDefaultDeliveryDate->format('Y-m-d'))) {
// will use newly calculated default delivery date
$productInfo['delivery_date'] = $calculatedDefaultDeliveryDate->format('Y-m-d');
} else {
$productInfo['delivery_date'] = $productInfo['expected_delivery_date'];
}
}
}
Let’s analyse the above old code and mark the places where issues could occur.
- The function parameter is having no type definition. Although the doc blof for the function defines what is expected as a valid parameter but that does not restrict it.
- On line #22 we may get array key does not exist error.
- On line #29 we are using strtotime and
$productInfo['default_delivery_date']
may not exist.
We could solve all of the above issues just doing inline check but I would suggest to change the function signature and pass a proper typed parameter. Let’s see how we could do that in the code block below.
class ProductInfoDTO
{
/**
* delivery after x days (product attribute)
*/
public int $deliveryAfter = 1;
/**
* customer suggested delivery date
*/
public ?DateTime $expectedDeliveryDate = null;
/**
* system generated delivery date
*/
public DateTime $defaultDeliveryDate;
/**
* actual delivery date
*/
public ?DateTime $deliveryDate = null;
public function __construct(
int $deliveryAfter, // the product attribute should be passed here
DateTime $expectedDeliveryDate = null
) {
$this->deliveryAfter = $deliveryAfter;
$currentDate = new DateTime();
$this->defaultDeliveryDate = date_add(
$currentDate,
DateInterval::createFromDateString($this->deliveryAfter . ' day')
);
$this->expectedDeliveryDate = $expectedDeliveryDate;
}
}
function adjustDeliveryDate(ProductInfoDTO $productInfo)
{
if ($productInfo->expectedDeliveryDate) {
if (strtotime($productInfo->expectedDeliveryDate->format('Y-m-d')) < strtotime($productInfo->defaultDeliveryDate->format('Y-m-d'))) {
$productInfo->deliveryDate = $productInfo->defaultDeliveryDate;
} else {
$productInfo->deliveryDate = $productInfo->expectedDeliveryDate;
}
} else {
$productInfo->deliveryDate = $productInfo->defaultDeliveryDate;
}
}
$productInfo = new ProductInfoDTO(
5,
date_add(new DateTime(), DateInterval::createFromDateString('6 day'))
);
adjustDeliveryDate($productInfo);
echo "<pre>";
var_dump($productInfo);
Conclusion
We are moving towards more advanced PHP, that’s great.