Я создаю функцию-обертку вокруг mysqli, чтобы мое приложение не было слишком сложным с точки зрения кода обработки базы данных. Частью этого является часть кода для параметризации вызовов SQL с помощью mysqli::bind_param(). bind_param(), как вы, возможно, знаете, требует ссылок. Поскольку это полуобщая обертка, в итоге я делаю такой вызов:
call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);
и получаю сообщение об ошибке:
Parameter 2 to mysqli_stmt::bind_param() expected to be a reference, value given
Приведенное выше обсуждение предназначено для тех, кто скажет: "В вашем примере ссылки вообще не нужны".
Мой "настоящий" код немного сложнее, чем кто-либо хочет читать, поэтому я скомпоновал код, приводящий к этой ошибке, в следующий (надеюсь) наглядный пример:
class myclass {
private $myarray = array();
function setArray($vals) {
foreach ($vals as $key => &$value) {
$this->myarray[] =& $value;
}
$this->dumpArray();
}
function dumpArray() {
var_dump($this->myarray);
}
}
function myfunc($vals) {
$obj = new myclass;
$obj->setArray($vals);
$obj->dumpArray();
}
myfunc(array('key1' => 'val1',
'key2' => 'val2'));
Проблема, похоже, в том, что в myfunc(), между вызовом setArray() и вызовом dumpArray(), все элементы в $obj->myarray перестают быть ссылками и становятся просто значениями. Это можно легко увидеть, взглянув на вывод:
array(2) {
[0]=>
&string(4) "val1"
[1]=>
&string(4) "val2"
}
array(2) {
[0]=>
string(4) "val1"
[1]=>
string(4) "val2"
}
Обратите внимание, что в первой половине вывода массив находится в "правильном" состоянии. Если бы это имело смысл, я мог бы сделать вызов bind_param() в этот момент, и все бы заработало. К сожалению, во второй половине вывода что-то ломается. Обратите внимание на отсутствие знака "&" в типах значений массива.
Что случилось с моими ссылками? Как я могу предотвратить это? Я ненавижу называть "ошибку PHP", когда я действительно не являюсь экспертом по языку, но может ли это быть ошибкой? Мне это кажется очень странным. В данный момент я использую PHP 5.3.8 для тестирования.
Редактировать:
Как указал не один человек, исправление заключается в изменении setArray(), чтобы она принимала свой аргумент по ссылке:
function setArray(&$vals) {
Я добавляю эту заметку, чтобы объяснить, почему это работает.
PHP в целом и mysqli в частности, похоже, имеют несколько странное представление о том, что такое "ссылка". Обратите внимание на этот пример:
$a = "foo";
$b = array(&$a);
$c = array(&$a);
var_dump($b);
var_dump($c);
Прежде всего, я уверен, что вам интересно, почему я использую массивы, а не скалярные переменные - это потому, что var_dump() не показывает, является ли скалярная переменная ссылкой, но показывает для членов массива.
В любом случае, на данный момент $b[0] и $c[0] являются ссылками на $a. Пока все хорошо. Теперь мы бросаем первый ключ в работу:
unset($a);
var_dump($b);
var_dump($c);
$b[0] и $c[0] по-прежнему являются ссылками на одно и то же. Если мы изменим одну из них, обе все равно изменятся. Но на что они ссылаются? На какое-то безымянное место в памяти. Конечно, сборка мусора гарантирует, что наши данные в безопасности, и будут оставаться таковыми, пока мы не перестанем ссылаться на них.
Для нашего следующего трюка мы сделаем следующее:
unset($b);
var_dump($c);
Теперь $c[0] - единственная оставшаяся ссылка на наши данные. И, вау! Волшебным образом это больше не "ссылка". Не по меркам var_dump(), и не по меркам mysqli::bind_param() тоже.
В руководстве по PHP говорится, что для каждой части данных существует отдельный флаг 'is_ref'. Однако этот тест предполагает, что 'is_ref' фактически эквивалентен '(refcount > 1)'
Для развлечения можно изменить этот игрушечный пример следующим образом:
$a = array("foo");
$b = array(&$a[0]);
$c = array(&$a[0]);
var_dump($a);
var_dump($b);
var_dump($c);
Обратите внимание, что все три массива имеют метку ссылки на своих членах, что подтверждает идею о том, что 'is_ref' функционально эквивалентен '(refcount > 1)'.
Мне непонятно, почему mysqli::bind_param() заботится об этом различии в первую очередь (или, возможно, это call_user_func_array()... в любом случае), но похоже, что на самом деле нам нужно убедиться, что количество ссылок не меньше 2 для каждого члена $this->bindArgs в нашем вызове call_user_func_array() (см. самое начало сообщения/вопроса). И самый простой способ сделать это (в данном случае) - сделать setArray() pass-by-reference.
Edit:
Для дополнительного веселья и игры я модифицировал свою оригинальную программу (здесь не показано), чтобы оставить эквивалент setArray() pass-by-value, и создать безвозмездно дополнительный массив bindArgsCopy, содержащий точно то же самое, что и bindArgs. Это означает, что, да, оба массива содержали ссылки на "временные" данные, которые были деаллоцированы к моменту второго вызова. Как и предсказывал приведенный выше анализ, это сработало. Это показывает, что приведенный выше анализ не является артефактом внутренней работы var_dump() (по крайней мере, для меня это облегчение), а также демонстрирует, что важно именно количество ссылок, а не "временность" исходного хранилища данных.
Итак. Я делаю следующее утверждение: В PHP, для целей call_user_func_array() (и, возможно, не только), сказать, что элемент данных является "ссылкой" - это то же самое, что сказать, что количество ссылок элемента больше или равно 2 (игнорируя внутренние оптимизации памяти PHP для равнозначных скаляров)
Примечание администратора: Я бы с удовольствием отдал должное mario the site за этот ответ, поскольку он первым предложил правильный ответ, но поскольку он написал его в комментарии, а не в самом ответе, я не могу этого сделать :-(