In the previous post, I discussed the considerations for upgrading a code base to a new version of PHP and mentioned a couple of tools. In this post, I am going to go into detail about my approach and experience in upgrading the webERP project from PHP 5.1 to PHP 7.0.9.

First, I installed the tools that I will need.

Composer is a package manager for PHP. I use it to install and update all of the PHP tools that I use (that are available as Composer packages). You can download Composer from here. I put the Composer file in my $HOME/bin directory so that it is in my PATH.

Just to confirm that Composer is is working, I ran composer.phar. Running it without arguments shows the help information.

As a side note, the Windows version of PHP does not play nice with Cygwin unix file system paths. To correct this problem, I wrote a shell script that converts any PHP command line argument that is an existing file to a Windows style path. I put this script in my $HOME/bin directory so that it is the default php program in my PATH. In order to keep this script from calling itself, I set the full path to PHP in a variable in .bash_profile.

#~/.bash_profile

export REAL_PHP_PATH="/cygdrive/c/development/languages/php709VC14ts"
PATH="${REAL_PHP_PATH}:${PATH}"
if [ -d "${HOME}/bin" ] ; then
    PATH="${HOME}/bin:${PATH}"
fi
# ~/bin/php

CMD="${REAL_PHP_PATH}/php ";

while true
do
    if [ $# -eq 0 ]
    then
        break
    fi

    ARG=$1
    if [ -f "$1" ]
    then
        ARG=`cygpath -w "$1"`
    fi

    CMD="${CMD} ${ARG}"

    shift
done

${CMD}

With the above set up, when I run this command:

php -f /cygdrive/c/path/to/script

The path is converted and this command actually gets executed:

/cygdrive/c/development/languages/php709VC14ts/php -f C:\path\to\script

Next, I installed PHPCompatibility. I manage all of my environment set up scripts in a couple of directories as git repositories. One of the main directories is the .vim directory where I maintain my Vim configuration files and plugins. I decided to install all of my PHP tools in the same directory so that they are portable with my Vim environment. Inside my .vim directory, I created a directory called phptools. In the phptools directory, I created a composer.json file. This is the instruction file to tell Composer what to install.

This is my composer.json file to install PHPCompatibility and it’s dependencies.

{
    "name": "dhanks/phptools",
    "description": "PHP Tools for VIM integration",
    "require-dev": {
        "squizlabs/php_codesniffer": "@stable",
        "wimg/php-compatibility": "@stable"
    }
}

To install or update these tools I can just run the Composer command.

composer.phar update

Composer creates a vendor subdirectory where it installs all of the packages it manages. Since my .vim directory is a git repository, I want to make sure that the vendor directory and it’s contents are not committed. I only want the configuration files committed. So, I created a .gitignore file in the phptools directory and added the vendor directory.

vendor/

The first time I ran Composer with the new install of PHP 7.0.9, it complained about missing the openssl module. I just edited my php.ini file to enable that module to fix that problem. One other note about Composer is that it can update itself. When you run Composer, it will warn you if your version is out of date. You can update composer using the self-update command argument.

That’s it for installation. Now I’m moving into the webERP project directory to start evaluating the code for the PHP Version upgrade.

The first thing I did was to create a new branch for the changes I’m going to make during this PHP version upgrade step.

git checkout -b upgrade modernize

I started the investigation with the PHP lint check. This is the most basic validation that can be done on the source code just to verify that it will compile. In order to check all of the files that contain PHP code, I first needed to identify all of the file extensions that need to be included in the lint check. I used this brute force command to get a list file extensions in the project. The output showed me that I needed to check *.php and *.inc files.

ls -R | egrep -v '(:$|\/$)' | sed 's/^.*\(\..*\)/\1/' | sort -u | less

To run the PHP lint check on all of the target files, I ran this command:

find . \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \;

The command produced output for every file, whether there were errors found or not. To limit the output to only errors, I added grep to the command.

find . \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \; | grep -v "^No syntax errors"

There was still a lot of output, but it looked like there were only a few different type of errors found. In order to get a more condensed idea of the types of problems, I ran the command again and reduced the output to only the category of error. The category is at the beginning of the output line followed by a colon. I did that with this updated command.

find . \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \; | \
    grep -v "^No syntax errors" | \
    awk 'BEGIN {FS=":"} {print $1}' | \
    sort -u

The command produced this output.

Deprecated
Errors parsing reportwriter\WriteReport.inc
Parse error

With a list of the categories of errors, I composed separate commands to isolate each error type so that I could work on them individually.

# Get Deprecated errors
find . \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \; | grep "^Deprecated: "
# Get Parse errors
find . \( -name '*.php' -o -name '*.inc' \) -exec php -l {} \; | grep "^Parse error: "

From reviewing the full output before, I only saw one Parse error, so I chose to work on it first. Running the command above gave me the full error message.

Parse error: Invalid numeric literal in reportwriter\WriteReport.inc on line 307

This is the code around line 307 in the WriteReport.inc file

    7     // fetch date filter info
    6     $df = $Prefs['DateListings']['fieldname'];
    5     $Today = date('Y-m-d', time());
    4     $ThisDay = mb_substr($Today,8,2);
    3     $ThisMonth = mb_substr($Today,5,2);
    2     $ThisYear = mb_substr($Today,0,4);
    1     // find total number of days in this month
>>307     if ($ThisMonth==04 OR $ThisMonth==06 OR $ThisMonth==09 OR $ThisMonth==11) { $TotalDays=30; }
    1     elseif ($ThisMonth==02 AND date('L', $t)) { $TotalDays=29; } // Leap year
    2     elseif ($ThisMonth==02 AND !date('L', $t)) { $TotalDays=28; }
    3     else { $TotalDays=31; }

This was an interesting one. While investigating this error, I found that only the 09 value was causing this error. After some research, I found that literal numbers in PHP with a leading 0 are interpreted as octal values and 09 is not a valid octal value. Before PHP 7, this was a silent error. In PHP 7, it was changed to emit a parse error.

From the PHP Manual:

Prior to PHP 7, if an invalid digit was given in an octal integer (i.e. 8 or 9), the rest of the number was ignored. Since PHP 7, a parse error is emitted.

To fix this, I just wrapped the value in quotes. I went ahead and did this for all of the literal values in the condition since the variables being compared are string values based on the assignment statements.

After making this change, I reran the command to check for Parse errors. It still returned an error in the same file, but this time it was a different line number.

    3         case "h": // RPT_GROUP_QUARTER
    2             $QtrStrt = intval(($ThisMonth-1)/3)*3+1;
    1             $QtrEnd = intval(($ThisMonth-1)/3)*3+3;
>>366             if ($QtrEnd==04 OR $QtrEnd==06 OR $QtrEnd==09 OR $QtrEnd==11) { $TotalDays=30; }
    1             $qs = date('Y-m-d', mktime(0,0,0, $QtrStrt, 1, $ThisYear));
    2             $qe = date('Y-m-d', mktime(0,0,0, $QtrEnd, $TotalDays, $ThisYear));
    3             $d = $df.">='".$qs."'";
    4             $d .= " AND ".$df."<='".$qe."'";
    5             $fildesc = ' '.RPT_DATERANGE.' '.RPT_FROM.' '.ConvertSQLDate($qs).' '.RPT_TO.' '.ConvertSQLDate($qe)      .';';
    6             break;

The problem on this line was the same, but I fixed it a different way. Since the variables being compared here are converted to integer values, I removed the 0 from the literal instead of wrapping it in quotes.

After this change, Parse error command returned no errors. I committed this change and moved on to the next error type.

Running the command to get the Deprecated errors produced a lot of results. The command takes several second to run, so I redirected the output of the command to a file so I could work with the results more quickly. All of the error messages looked like this.

Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; PDF has a deprecated constructor in reportwriter\WriteReport.inc on line 4

There were 62 of these errors. All of the errors were the same and the fix was the same: rename the constructor method from the name of the class to __construct. Having 62 of these to fix, I spent some time to figure out how to automate this change.

The error message has the name of the file, the name of the class (which is also the method name), and the line number. However the line number is the line number of the class declaration, not the method declaration, so I couldn’t use it. I needed to pull the file and the class name from each error message and use that to search and replace the function declaration for the constructor from the class name to __construct. This is what I came up with.

read INPUT
ARGS=`echo "
<?php
preg_match('/^Deprecated:.*PHP; (.*) has a deprecated.* in (.*) on line [0-9].*\$/', '${INPUT}', \\$matches);
echo \\$matches[1] . ' ' . \\$matches[2];
?>
" | php`

CLASS=$(echo ${ARGS} | cut -f1 -d' ')
FILE=$(echo ${ARGS} | cut -f2 -d' ')

sed -i "s/function\s\+${CLASS}\(\s*(\)/function __construct\1/" ${FILE}

I tested the script with the first error message from the file I created and the result looked good.

head -1 deprecated_errors.txt | ./fixDeprecated.sh
git diff includes/DefineCartClass.php
diff --git a/includes/DefineCartClass.php b/includes/DefineCartClass.php
index 43439a5..955f693 100644
--- a/includes/DefineCartClass.php
+++ b/includes/DefineCartClass.php
@@ -62,7 +62,7 @@ Class Cart {
        var $BuyerName;
        var $SpecialInstructions;

-       function Cart(){
+       function __construct(){
        /*Constructor function initialises a new shopping cart */
                $this->LineItems = array();
                $this->total=0;

I ran the script for all of the error messages and reviewed the git diff for any unexpected changes.

while read error; do echo "$error" | ./fixDeprecated.sh; done <deprecated_errors.txt

The results were good. I reran the command to perform the lint check on all of the files. There was one error still being reported. Looking at this one error individually, I found that the case of the class name and method name did not match. The pattern I put into the script above could not find this to fix it. I just fixed this one manually, reran the lint command again and got a clean report.

With the errors fixed and a clean lint report, I committed the changes. But, I couldn’t stop there. Because I changed some method names, I needed to do some validation to makes sure that those names were not used anywhere in the code. I know they were constructors, but they could be called directly from child classes or from other code as instance or static methods. What I needed to do was to check that none of the class names from the error messages were called as instance or static methods anywhere in the code. I wrote another script for this.

read INPUT
ARGS=`echo "
<?php
preg_match('/^Deprecated:.*PHP; (.*) has a deprecated.* in (.*) on line [0-9].*\$/', '${INPUT}', \\$matches);
echo \\$matches[1] . ' ' . \\$matches[2];
?>
" | php`

CLASS=$(echo ${ARGS} | cut -f1 -d' ')
FILE=$(echo ${ARGS} | cut -f2 -d' ')

echo "Searching for Class: ${CLASS}"
egrep -r -- "(->${CLASS}\(|->${CLASS} \(|::${CLASS}\(|::${CLASS} \()" *
echo
echo

I found one case where a child class was calling the constructor of the parent class using the old class name method. In this case I fixed the problem by replacing the call with parent::__construct().

That’s it. The PHP Lint check on the source code is complete. All the changes are committed. I clicked through the application and saw no problems. The next step is to check the code with PHPCompatibility. That will be the next post. If you have any insights or suggestions on how to validate code for PHP version upgrades, please share in the comments.

Share this on: