Janis T
Janis T

Reputation: 1083

Migration to PHP 8.1 - how to fix Deprecated Passing null to parameter error

PHP 8.1 has deprecated passing null as a parameter to a lot of core functions. My main problem is with functions like htmlspecialchars and trim, where null is no longer silently converted to the empty string.

To fix this issue without going through a huge amount of code I was trying to rename the original built-in functions and replace them with wrappers that cast input from null to (empty) string.

My main problem with this approach is, that the function rename_function (from PECL apd) no longer works; last update on this is from 2004 1.

I need some sort of override of built-in functions, to avoid writing null checks each time a function is called making all my code two times larger.

The only other solution I can think of is to use only my custom functions, but this still require going through all my code and third party libraries I have.


Upvotes: 98

Views: 236350

Answers (14)

Mahendra Singh
Mahendra Singh

Reputation: 61

Question : Deprecated: Assert\that(): Implicitly marking parameter $defaultPropertyPath as nullable is deprecated, the explicit nullable type must be used instead in

And For Solving this issue, follow below steps.

  1. Open php.ini file and

  2. Add below line error_reporting = E_ALL & ~E_DEPRECATED

  3. Restart the apache server on windows.

This will remove all the Deprecated: warning issue with phpmyadmin

Upvotes: -1

Michael Bolli
Michael Bolli

Reputation: 2149

Rector has the rule NullToStrictStringFuncCallArgRector to fix this:

-     mb_strtolower($value);
+     mb_strtolower((string) $value);

Upvotes: 10

Nux
Nux

Reputation: 10002

While ?? is cool I think doing is_string($val) check might be more appropriate.

You can use regexp replacement to change all occurrences (or at least all typical occurrences).

Find and replace:

  • htmlspecialchars\((\$\w+)\)
  • (!is_string($1) ? '' : htmlspecialchars($1))

This would replace htmlspecialchars($val); with (!is_string($val) ? '' : htmlspecialchars($val)).

Upvotes: 0

Jodyshop
Jodyshop

Reputation: 664

After searching a lot and trying too many solutions, here is the final solution for this issue until they release a newer compatible version of Smarty templates... Add to the sitewide function.php file.

function custom_trim(?string $value) {
    return empty($value) ? '' : trim($value);
}

Upvotes: 0

vr_driver
vr_driver

Reputation: 2085

What a painful experience this is.

Here's my quick solution which is compatible with older versions of php:

function custom_trim(  $value)
{

    if($value!="")
    {
        $value = trim($value);
    }
    return $value;
} 


function custom_stripslashes(  $value)
{

    if($value!="")
    {
        $value = stripslashes($value);
    }
    return $value;
} 
 

Upvotes: 2

Dennis Kopitz
Dennis Kopitz

Reputation: 11

We recently updated from php 7.4 to 8.1 (on Magento 2.4.4). We also experienced lots of exceptions thrown for deprecation warnings within vendor modules and our extensive in-house custom modules.

We remediated everything we could find, however to be safe, we implemented the following patch, which modifies magento's error handler to log these deprecation warnings, rather than throwing them as exceptions.

On go-live in production, this kept the application running (by simply logging these errrors), and gave us breathing room to address them as we saw them occuring in the logs.

Keep in mind that magento cli does not use this error handler, but rather the symphony framework one, which luckly handles deprecation warnings by outputing them to the console (so watch for those as well).

Upgrade went smooth, so maybe this will helps others...

Index: vendor/magento/framework/App/Bootstrap.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/vendor/magento/framework/App/Bootstrap.php b/vendor/magento/framework/App/Bootstrap.php
--- a/vendor/magento/framework/App/Bootstrap.php
+++ b/vendor/magento/framework/App/Bootstrap.php    (date 1679064575518)
@@ -384,7 +384,7 @@
      */
     private function initErrorHandler()
     {
-        $handler = new ErrorHandler();
+        $handler = new ErrorHandler($this->objectManager->get(LoggerInterface::class));
         set_error_handler([$handler, 'handler']);
     }


Index: vendor/magento/framework/App/ErrorHandler.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/vendor/magento/framework/App/ErrorHandler.php b/vendor/magento/framework/App/ErrorHandler.php
--- a/vendor/magento/framework/App/ErrorHandler.php
+++ b/vendor/magento/framework/App/ErrorHandler.php (date 1679073448870)
@@ -34,6 +34,15 @@
         E_USER_DEPRECATED => 'User Deprecated Functionality',
     ];

+    private $logger;
+
+    public function __construct(
+       $logger = null
+    )
+    {
+        $this->logger = $logger;
+    }
+
     /**
      * Custom error handler
      *
@@ -50,6 +59,12 @@
             // there's no way to distinguish between caught system exceptions and warnings
             return false;
         }
+
+        if (E_DEPRECATED == $errorNo) {
+            $msg = "Logging - PHP Deprecation Warning: {$errorStr} in {$errorFile} on line {$errorLine}";
+            if ($this->logger) $this->logger->warning($msg);
+            return false;
+        }

         $errorNo = $errorNo & error_reporting();
         if ($errorNo == 0) {

Upvotes: 1

chispitaos
chispitaos

Reputation: 937

A solution for existing projects with a lot of pages, that you want to migrate to PHP8+:

In my case, most problems came with "trim" function that receives null values. In that case, you can create a custom "trim" function and then replace in your existing code the "trim" for "custom_trim" function:

public function custom_trim(?string $value)
{
    return trim($value ?? '') ;
} 

or just cast the parameter like this

   trim((string) $value);

Upvotes: 8

Ajay Maurya
Ajay Maurya

Reputation: 611

WPCS is incompatible with PHP 8.1. Adding this to your phpcs config file may fix it for you.

<ini name="error_reporting" value="E_ALL &#38; ~E_DEPRECATED" />

(Note that the &#38; is an escaped & (ampersand); the & character must be escaped in XML documents.)

Reference - https://github.com/WordPress/WordPress-Coding-Standards/issues/2035#issuecomment-1325532520

Upvotes: 1

hakre
hakre

Reputation: 197533

I'd like (as an addition, existing answers have my upvotes) to paint a different picture on how to see and tackle with such "problems". It does not make the outlined approaches less right or wrong and is merely an additional view which hopefully is of mutual benefit. And every project is different.

Given the premise:

My main problem is with functions like htmlspecialchars(php) and trim(php), where null no longer is silently converted to the empty string.

then this looks (first of all) as a reporting problem to me. The code can be made silent by not reporting E_DEPRECATED.

Doing so ships (not only your code) with the benefit, that it is now known that your code is with deprecation notices. Reporting did work.

On the other hand, silencing deprecation notices may move them out of sight. And if you loose the information that the code-base is with deprecation notices, it may still be technically easy to recover from that loss of information (report deprecation notices again), however if the time from change has enlarged, there might now be an overwhelming noise (E_TOO_MUCH_NOISE).

So is the code not being silent actually a bad thing? Or can it be turned into a benefit? I'd prefer to go with the later. We're working with the information already anyway.

So in this case I had the idea to not generally suppress the deprecation notices but to "silence" the function calls. It is easy, and there is stupidity both in the good but also in the worse sense with it:

trim($mixed);   #1  ->     @trim($mixed);   #2

This is certainly an operation that can be applied on a code-base with standard text tooling. It would also show you where use of the @ suppression operator already was made in the past:

@trim($mixed);  #3  ->     @@trim($mixed);  #4

If you're a PHP developer looking at such code within your editor (for cases #2-#4), they would immediately scream to you and for all four cases raise your eyebrows at least ($mixed).

So thanks for not being silent, we made the places screaming, just not at runtime1.

Different to the first approach to silence by not reporting E_DEPRECATED which is prone to loosing information, the information is preserved by having all the @-signs.

Does it help with the noise problem? If we stop doing the work here, not at all. Now we would have plastered the code with @-signs, decided to take no further action so we could have already done it with the first solution (do not report deprecation message) without touching the code.

So what is the benefit of it? Well, despite the code now running silent, PHP still is providing the diagnostic messages. That is, it is now possible to register a PHP error-handler as a listener (when executing the code).

And just on code-level, it is easy to review the places as the @-signs are (typically) easy to spot in the code as well.

The second part is important, as albeit multiple places may be affected by a deprecation, there must not be one fix to catch them all (me prefers to stay away from "one size fits it all 'solutions'" if possible), but especially with this PHP 8.1 change in context of the question, I can imagine there are different needs depending on place of use.

For example in templating code (output) the concrete type is less an issue, and therefore a cast to string is very likely the preferred fix:

@trim($mixed);     ->     trim((string)$mixed)
@@trim($mixed);    ->     @trim((string)$mixed)

The templating (output) remains stable.

But for actual input processing, the deprecation notice may uncover actual underlying flaws that are worth to fix, like missing defaults (over complicating things), unclear handling of values (empty vs. null vs. string vs. bool vs. number vs. array vs. object in PHP) or a general $mixed confusion.

Such a trim($mixed) could be a years old forgotten safe-guard that has never undergone the upgrade (there are better safeguards available). For such code I'm pretty sure I already want and demand that $mixed actually is $string before I make use of trim(). The reason is simple, at least two things are coming to mind directly:

  • a) Either trim() is not necessary any more - than it can be removed (one of my favorite fixes: removing code!) - or -
  • b) It is doing string-work then I have a problem as I don't want anything not a string to be there. Problem in the sense, that it is often not applicable with a shotgun approach (Gießkanne anyone?).

It is totally valid to patch with $mixed ?? '' if the original use was string or null only.

@trim($mixed);     ->     trim($mixed ?? '')
@@trim($mixed);    ->     @trim($mixed ?? '')

But otherwise, e.g. a number like 42, instead of a deprecation message, a TypeError is being thrown. This can make the difference between running and not running code.

So there is a bit more to maintain here, like reviewing the places, further clustering if possible and then applying more dedicated fixes. It could reveal missing tests or assertions, need a bit of a time to stabilize in the overall application flow etc..

Under such cases to complete the migration of the code, do the clustering, handle w/ null-coalescing operator and do the proper paper-work for the real fixes. Once the non-obvious error suppression with the null-coalescing operator has been done and the @ suppression operator removed, you'll likely loose the information if the fix planning has not captured them.

When looking more educated at such places, I'm not surprised when I find myself scratching head or rubbing my eyes. Then I remind myself that those errors are not because of the PHP 8.1 version, the version change has just brought them up (again) and I get sometimes even complete bug clusters as a by-catch only by maintaining the PHP version.

Cheat-Sheet

  • (string)$mixed - previous behaviour
  • $mixed ?? '' - error suppression for TypeError on null only
  • @ - full error suppression. you should document in/for your code-base where it is applicable to use.
  • @@ - if this comes up, it is likely an interesting spot to look into.
  • empty($mixed) ? '' : xxx($mixed) - carry the trash out, typical empty paralysis / mixed confusion, look for clusters, there is a chance the code-base can be greatly simplified. migrate to scalar types (PHP 7), pull in strict type handling from most-inner in outwards direction, use both PHP "classic" and "strict" type handling where applicable. PHP 7.0 assertions and PHP 8.1 deprecation messages can support here well.

Error Handler

There is no magic with the error handling, it is standard as documented on PHP.net (compare with Example #1), it works as an observer on the error events, distinction between suppressed and non-suppressed errors can be done via error_reporting(php) / error_reporting(php-ini) at least to the level normally necessary if the distinction is needed (in a production setting E_DEPRECATED is normally not part of the reporting). This exemplary handler throws on all reported errors, so would for deprecation events as well for E_ALL and would therefore require the @ suppression operator to not throw:

set_error_handler(static function ($type, $message, $file, $line) use (&$deprecations) {
    if (!(error_reporting() & $type)) {
        // This error code is not included in error_reporting, so let it fall
        // through to the standard PHP error handler

        // capture E_DEPRECATED
        if ($type === E_DEPRECATED) {
            $deprecations[] =
                ['deprecations' => count($deprecations ?: [])]
                + get_defined_vars();
        }

        return false;
    }

    // throwing error handler, stand-in for own error reporter
    // which may also be `return false;`
    throw new ErrorException($message, $type, error_reporting(), $file, $line);
});

An error handler similar to it can be found in an extended example on 3v4l.org including deprecated code to report on.

E_USER_DEPRECATED

Technically, the error suppression operator can be combined with E_USER_DEPRECATED the same as outlined with E_DEPRECATED above.

However there is less control about it and it may already be in use by third-party code a project may already have in its dependencies. It is not uncommon to find code similar to:

@trigger_error('this. a message.', E_USER_DEPRECATED);

which does exactly the same: emit deprecation events but exclude them from PHPs' reporting. Subscribing on those may put you into the noise. With E_DEPRECATED you always get the "good, original ones" from PHP directly.


  1. When considered the approach with the @ error suppression operator and commented on it, IMSoP immediately raised a red/black flag (rightfully!) that it is easy to throw the baby out with the bathwater with the @ suppression operator. In context of my answer it is intended to only suppress the deprecation notice but the consequence of use is that it suppresses all diagnostic messages and errors, in some PHP versions even fatal ones, so PHP exits 255 w/o any further diagnostics - not only take but handle with care. This operator is powerful. Track its usage in your code-base and review it constantly that it matches your baseline/expectations. For legit cases consider to make use of a silencer. For porting / maintaining the code use it for flagging first of all. When done with mass-editing, remove it again.

Upvotes: 8

ArthurV
ArthurV

Reputation: 1

Another option is to create a phpFunctionWrapper class that you can inject through the constructor of your class. The wrapper functions should take care of the null coalescent operator rather than introduce this dependency in the code.

For example:

<?php

namespace Vendor\Core\Helper;

class PhpFunctionWrapper
{
    public function numberFormat($number, $decimals): string|false {
        return number_format($number ?? 0.0, $decimals);
    }

    public function strPos($haystack, $needle, int $offset = 0): int|false {
        return strpos( $haystack ?? "", $needle ?? "", $offset);
    }

    public function pregSplit($pattern, $subject, $limit = -1, $flags = 0): array|bool
    {
        return preg_split($pattern ?? '', $subject ?? '', $limit, $flags);
    }

    public function explode($separator, $string, $limit = PHP_INT_MAX): array
    {
        return explode($separator, $string, $limit);
    }
}

Then, you inject the wrapper class in your class through the constructor:

<?php

namespace Vendor\Catalog\Block\Product;

use Vendor\Core\Helper\PhpFunctionWrapper;
use Magento\Catalog\Block\Product\Context;
use Magento\Catalog\Api\ProductRepositoryInterface;

class View extends \Magento\Catalog\Block\Product\View
{
    private PhpFunctionWrapper $phpFunctionWrapper;

    public function __construct(Context                                             $context,
                                \Magento\Framework\Url\EncoderInterface             $urlEncoder,
                                \Magento\Framework\Json\EncoderInterface            $jsonEncoder,
                                \Magento\Framework\Stdlib\StringUtils               $string,
                                \Magento\Catalog\Helper\Product                     $productHelper,
                                \Magento\Catalog\Model\ProductTypes\ConfigInterface $productTypeConfig,
                                \Magento\Framework\Locale\FormatInterface           $localeFormat,
                                \Magento\Customer\Model\Session                     $customerSession,
                                ProductRepositoryInterface                          $productRepository,
                                \Magento\Framework\Pricing\PriceCurrencyInterface   $priceCurrency,
                                PhpFunctionWrapper                                  $phpFunctionWrapper,
                                array                                               $data = [])
    {
        parent::__construct($context,
            $urlEncoder,
            $jsonEncoder,
            $string,
            $productHelper,
            $productTypeConfig,
            $localeFormat,
            $customerSession,
            $productRepository,
            $priceCurrency,
            $data);
        $this->phpFunctionWrapper = $phpFunctionWrapper;
    }
}

Finally, in for example a template file that uses the View block, you change the code from:

<div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $position : '' ?>>

to:

<div data-role="add-to-links" class="actions-secondary"<?= $block->phpFunctionWrapper->strPos($pos, $viewMode . '-secondary') ? $position : '' ?>>

Of course, you need to find all occurrences, but you need to go through them anyway. At least in the future, if you need to change something about these functions, you only need to change the wrapper.

I have created a core helper module where I keep these types of solutions that I can inject where needed. It keeps my code clean, and free from dependencies.

Upvotes: 0

Labhansh Satpute
Labhansh Satpute

Reputation: 449

The problem was occured at vendor/laravel/framework/src/Illuminate/Routing/RouteGroup.php:65

You can fix this problem by casting the variable to a string using (string)

Like before it was trim($old, '/') After casting trim((string)$old, '/')

protected static function formatPrefix($new, $old, $prependExistingPrefix = true)
{
    $old = $old['prefix'] ?? null;

    if ($prependExistingPrefix) {
        return isset($new['prefix']) ? trim((string)$old, '/').'/'.trim((string)$new['prefix'], '/') : $old;
    } else {
        return isset($new['prefix']) ? trim((string)$new['prefix'], '/').'/'.trim((string)$old, '/') : $old;
    }
} 

Upvotes: 0

Dieter Donnert
Dieter Donnert

Reputation: 302

The OP's issue is that refactoring a large code base is hard. Adding ??'' to every strlen() call is a major time sink when your dealing with many MB of legacy source code.

Type conversion works on null values such that

strlen((string)null); // returns 0

A search and replace of strlen( with strlen((string) can work but you'd still need to step through them one at a time to look for edge cases.

Upvotes: 3

fred727
fred727

Reputation: 2819

while waiting to correct the problems (there may be many) it is possible to define a custom error handling function to ignore them.

For exemple :

error_reporting(E_ALL) ;
set_error_handler(
    function($severity, $message, $file, $line) {
        if ( !$severity || error_reporting()!=E_ALL ) return ;  // to treat @ before functions
        $erreurs_autorisees = array(
            E_NOTICE          => "Notice",
            E_USER_NOTICE     => "User Notice",
            E_DEPRECATED      => "Deprecated",
            E_USER_DEPRECATED => "User Deprecated",
            E_WARNING         => "Warning",
            E_USER_WARNING    => "User Warning",
        ) ;
        if ( isset($erreurs_autorisees[$severity]) ) {
            $warning_autorises = [
                "addslashes(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "base64_decode(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "htmlspecialchars(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "mb_decode_mimeheader(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "mysqli_real_escape_string(): Passing null to parameter #2 (\$string) of type string is deprecated",
                "preg_replace(): Passing null to parameter #3 (\$subject) of type array|string is deprecated",
                "preg_split(): Passing null to parameter #3 (\$limit) of type int is deprecated",
                "rawurlencode(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "setcookie(): Passing null to parameter #2 (\$value) of type string is deprecated",
                "str_starts_with(): Passing null to parameter #1 (\$haystack) of type string is deprecated",
                "strcmp(): Passing null to parameter #1 (\$string1) of type string is deprecated",
                "strlen(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "strtr(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "strpos(): Passing null to parameter #1 (\$haystack) of type string is deprecated",
                "substr(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "trim(): Passing null to parameter #1 (\$string) of type string is deprecated",
                "strncasecmp(): Passing null to parameter #1 (\$string1) of type string is deprecated",
            ] ;
            if ( in_array($message, $warning_autorises) ) return true ;
            
            // On ne converti pas les warning en Exception, on se contente de les logger / les afficher
            $msg = $erreurs_autorisees[$severity].": $message in $file on line $line" ;
            if ( ini_get('display_errors') ) echo $msg ;
            // @error_log($msg) ;  // if you want to log
        }
        else throw new ErrorException($message, 0, $severity, $file, $line) ;
        return true;
    }
);

Upvotes: 0

IMSoP
IMSoP

Reputation: 97628

Firstly, two things to bear in mind:

  1. PHP 8.1 deprecates these calls, it does not make them errors. The purpose of deprecation is to give authors advance notice to fix their code, so you and the authors of libraries you use have until PHP 9.0 comes out to fix things. So, don't panic that not everything is fixed right away, and be patient with library maintainers, who will get to this in their own time.
  2. The quick fix in most cases is to use the null coalescing operator to provide a default value as appropriate, so you don't need a long null check around every use. For instance, htmlspecialchars($something) can be replaced with htmlspecialchars($something ?? '')

Next, some options:

  • Depending how many cases you have, you may be able to just fix them manually a few at a time, either adding ?? '' or fixing a logic bug where you weren't expecting a null anyway.
  • Create custom functions like nullable_htmlspecialchars and do a straight-forward find and replace in your code.
  • Create custom namespaced functions like nullableoverride\htmlspecialchars; then in any file where you add use function nullableoverride\htmlspecialchars; that function will be used instead of the built-in one. This has to be added in each file, though, so you may need a tool to automate adding it.
  • Use Rector to automate adding ?? '' to appropriate function calls, so you don't have to edit them all by hand. Unfortunately, there doesn't seem to be a built-in rule for this (yet), so you'd have to learn to write your own.
  • Possibly simpler, depending on your skills, use a regular expression find-and-replace to add the ?? '' to simple cases.

Upvotes: 107

Related Questions