A function sees only its parameters and locals; reach a global with global $total or $GLOBALS['total'], or better, pass it in. A parameter declared &$sum is a reference to the caller's variable. Without &, arrays and strings are copy-on-write: copies share one buffer until one side writes. Objects are handles, so $b = $a already names the same object.
<?php
declare(strict_types=1);
$total = 0;
function addGlobal(int $n): void { global $total; $total += $n; }
function addByRef(int &$sum, int $n): void { $sum += $n; }
function addByValue(int $sum, int $n): void { $sum += $n; } // changes its own copy
addGlobal(5);
addByRef($total, 10);
addByValue($total, 100);
$tags = ['php', 'sql', 'css'];
foreach ($tags as &$tag) { $tag = strtoupper($tag); }
foreach ($tags as $tag) {} // $tag still aliases $tags[2]: the bug
echo "total $total, tags ", implode(',', $tags), "\n";
$a = range(1, 100_000);
$m = memory_get_usage();
$b = $a; // copy-on-write: both share one buffer
echo 'assign +', memory_get_usage() - $m, ' B, ';
$b[] = 0; // the first write makes the real copy
echo 'write +', intdiv(memory_get_usage() - $m, 1024), " KB\n";total 15, tags PHP,SQL,SQL assign +0 B, write +2052 KB
CSS became SQL: after the first loop $tag still refers to $tags[2], so the second loop copies each element into that slot. Put unset($tag); after every by-reference foreach.

Copy-on-write is why & rarely helps performance: a large array passed by value costs nothing until the function writes to it. Use references only when a function must change the caller's variable.