13

I encountered a strange behavior of Magento 1.8 after changing php version from 5.5 to 7.0. This strange behavior is due to a change in the work function uasort.

Source code:

<?php

$arr = [
    "nominal" => [
        "before" => ["subtotal", "grand_total"],
        "after" => [],
        "_code" => "nominal"
    ],
    "subtotal" => [
        "after" => ["nominal"],
        "before" => ["grand_total", "shipping", "freeshipping", "tax_subtotal", "discount", "tax", "weee"],
        "_code" => "subtotal"
    ],
    "shipping" => [
        "after" => ["subtotal", "freeshipping", "tax_subtotal", "nominal", "weee"],
        "before" => ["grand_total", "discount", "tax_shipping", "tax"],
        "_code" => "shipping"
    ],
    "grand_total" => [
        "after" => ["subtotal", "nominal", "shipping", "freeshipping", "tax_subtotal", "discount", "tax"],
        "before" => [],
        "_code" => "grand_total"
    ],
    "msrp" => [
        "before" => [],
        "after" => [],
        "_code" => "msrp"
    ],
    "freeshipping" => [
        "after" => ["subtotal", "nominal"],
        "before" => ["tax_subtotal", "shipping", "grand_total", "tax", "discount"],
        "_code" => "freeshipping"
    ],
    "discount" => [
        "after" => ["subtotal", "shipping", "nominal", "freeshipping", "tax_subtotal", "tax_shipping", "weee"],
        "before" => ["grand_total", "tax"],
        "_code" => "discount"
    ],
    "tax_subtotal" => [
        "after" => ["0" => "freeshipping", "1" => "subtotal", "3" => "nominal"],
        "before" => ["tax", "discount", "shipping", "grand_total", "tax_shipping", "weee"],
        "_code" => "tax_subtotal"
    ],
    "tax_shipping" => [
        "after" => ["shipping", "tax_subtotal", "subtotal", "freeshipping", "nominal"],
        "before" => ["tax", "discount", "grand_total"],
        "_code" => "tax_shipping"
    ],
    "tax" => [
        "after" => ["subtotal", "shipping", "discount", "tax_subtotal", "freeshipping", "tax_shipping", "nominal", "weee"],
        "before" => ["grand_total"],
        "_code" => "tax"
    ],
    "weee" => [
        "after" => ["subtotal", "tax_subtotal", "nominal", "freeshipping"],
        "before" => ["shipping", "tax", "discount", "grand_total", "tax_shipping"],
        "_code" => "weee"
    ]
];


function _compareTotals($a, $b)
{
    $aCode = $a['_code'];
    $bCode = $b['_code'];
    if (in_array($aCode, $b['after']) || in_array($bCode, $a['before'])) {
        $res = -1;
    } elseif (in_array($bCode, $a['after']) || in_array($aCode, $b['before'])) {
        $res = 1;
    } else {
        $res = 0;
    }
    echo sprintf("%s <> %s: %s", $aCode, $bCode, $res) . "\n";
    return $res;
}

uasort($arr, '_compareTotals');
var_dump(array_keys($arr));

In php 5.5 result is:

freeshipping <> subtotal: 1
freeshipping <> shipping: -1
weee <> freeshipping: 1
tax <> freeshipping: 1
tax_shipping <> freeshipping: 1
tax_subtotal <> freeshipping: 1
discount <> freeshipping: 1
nominal <> freeshipping: -1
freeshipping <> grand_total: -1
msrp <> freeshipping: 0
subtotal <> msrp: 0
nominal <> subtotal: -1
tax_subtotal <> shipping: -1
weee <> tax_subtotal: 1
tax <> tax_subtotal: 1
tax_shipping <> tax_subtotal: 1
grand_total <> tax_subtotal: 1
discount <> tax_subtotal: 1
shipping <> tax_subtotal: 1
grand_total <> discount: 1
grand_total <> shipping: 1
grand_total <> tax_shipping: 1
grand_total <> tax: 1
weee <> grand_total: -1
shipping <> discount: -1
tax <> shipping: 1
tax_shipping <> shipping: 1
weee <> shipping: -1
tax_shipping <> discount: -1
tax <> tax_shipping: 1
discount <> tax_shipping: 1
tax <> discount: 1

array(11) {
  [0] =>
  string(7) "nominal"
  [1] =>
  string(8) "subtotal"
  [2] =>
  string(4) "msrp"
  [3] =>
  string(12) "freeshipping"
  [4] =>
  string(12) "tax_subtotal"
  [5] =>
  string(4) "weee"
  [6] =>
  string(8) "shipping"
  [7] =>
  string(12) "tax_shipping"
  [8] =>
  string(8) "discount"
  [9] =>
  string(3) "tax"
  [10] =>
  string(11) "grand_total"
}

In php 7.0 result is:

nominal <> subtotal: -1
subtotal <> shipping: -1
shipping <> grand_total: -1
grand_total <> msrp: 0
msrp <> freeshipping: 0
freeshipping <> discount: -1
discount <> tax_subtotal: 1
msrp <> tax_subtotal: 0
freeshipping <> tax_subtotal: -1
discount <> tax_shipping: 1
freeshipping <> tax_shipping: -1
tax_subtotal <> tax_shipping: -1
discount <> tax: -1
tax <> weee: 1
tax_shipping <> weee: 1
freeshipping <> weee: -1
tax_subtotal <> weee: -1

array(11) {
  [0] =>
  string(7) "nominal"
  [1] =>
  string(8) "subtotal"
  [2] =>
  string(8) "shipping"
  [3] =>
  string(11) "grand_total"
  [4] =>
  string(4) "msrp"
  [5] =>
  string(12) "freeshipping"
  [6] =>
  string(12) "tax_subtotal"
  [7] =>
  string(4) "weee"
  [8] =>
  string(12) "tax_shipping"
  [9] =>
  string(8) "discount"
  [10] =>
  string(3) "tax"
}

In PHP5 grand_total is the last element, but in PHP7 - no. The problem is related to the uncertainty of the location msrp element. I found a research on this subject associated with php 5.

I solved this problem by indicating a relative position msrp. But I wonder why it works in php5 and does not work in php7. These are the features of the new version of php or a bug?

addition #1

The problem not only in that the PHP7 don't knows how to sort equal elements, for example msrp and grand_total. If you look at item shipping and freeshipping, then they are clearly defined, who should be earlier. PHP5 solves this problem, and PHP7 not.

Community
  • 1
  • 1
danil
  • 230
  • 2
  • 10
  • Interesting that PHP7 completes the sort in fewer steps; I wonder if this is coincidence, or if the revised algorithm is generally more efficient for this kind of data. – IMSoP Dec 15 '15 at 11:37
  • This is independent to PHP 7 and has been reported earlier, for example back in 2012: [Sort algorithm: Magento checkout totals sorted wrongly causing wrong shipping tax calculation](http://stackoverflow.com/q/9194281/367456), internal Magento ticket given is [MCACE-129]. – hakre Dec 22 '15 at 20:14
  • @hakre, yes, I wrote about it in my question. – danil Dec 23 '15 at 04:47

3 Answers3

12

From the usort() documentation:

Note: If two members compare as equal, their relative order in the sorted array is undefined.

This is what you're seeing here. PHP 7 uses a different, partially stable sorting algorithm, so elements that compare equal according to your sorting function may now have a different order.

If you care about the sorting order of equal elements (and this is not just a testing issue), you should make it explicit in your comparison function.

NikiC
  • 100,734
  • 37
  • 191
  • 225
  • The problem not only in that the PHP7 don't knows how to sort equal elements, for example `msrp` and `grand_total`. If you look at item `shipping` and `freeshipping`, then they are clearly defined, who should be earlier. PHP5 solves this problem, and PHP7 not. – danil Dec 15 '15 at 11:49
  • 2
    @danil, PHP can't sort your array properly if you say some things are equal when they aren't. Because your function returns 0 for some things which aren't equal, it messes up the rest of the sort. – Andrea Dec 15 '15 at 15:44
  • 2
    This is essentially a case of the [GIGO principle](https://en.wikipedia.org/wiki/Garbage_in,_garbage_out): if you give a the sorting function nonsense, you get nonsense. – Andrea Dec 15 '15 at 15:48
  • Was this actually called out somewhere in the release notes or docs for upgrading? I looked but could not find it. This made for some really confusing troubleshooting. – ChadSikorra Aug 15 '16 at 01:03
4

Possible Solution

I've created a magento module to solve magento issues with totals calculation for php7. The issues I've experienced in particular were that taxes have been added twice to the grand total for pay with amazon module on the amazonpayments checkout page.

Credits

The solution was provided by archigrafix on https://magento.stackexchange.com/a/97107/35665 solved my issues - so this is simply the fix packed into a module.

Module:

https://github.com/hartmut-ltd/magento-php7-totals-fix

Community
  • 1
  • 1
Hartmut
  • 725
  • 9
  • 11
0

for work in PHP7 you just have to call the function this way:

uasort($result, function ($a, $b) {
   your_sort_function($a, $b);   //call your sort function here
});

so modify your code like below:

function _compareTotals($a, $b)
{
    $aCode = $a['_code'];
    $bCode = $b['_code'];
    if (in_array($aCode, $b['after']) || in_array($bCode, $a['before'])) {
        $res = -1;
    } elseif (in_array($bCode, $a['after']) || in_array($aCode, $b['before'])) {
        $res = 1;
    } else {
        $res = 0;
    }
    echo sprintf("%s <> %s: %s", $aCode, $bCode, $res) . "\n";
    return $res;
}

uasort($arr, function($a, $b) {
    _compareTotals($a, $b);
});
var_dump(array_keys($arr));
Ramin eghbalian
  • 2,348
  • 1
  • 16
  • 36