Особенности работы magic methods в моделях Phalcon 2

Phalcon - молодой PHP-фреймворк, выполненный в виде PHP-расширения. Он работает намного быстрее из-за того, что все основные операции выполняются на системном уровне. Однако, из-за того, что он молодой, иногда встречаются странные вещи. Вот об одном таком баге я сегодня расскажу.

Предыстория

Летом мы в команде начали разрабатывать проект на Phalcon 2. В этом проекте установка даты рождения пользователя реализована следующим образом: в браузере работает плагин календаря, который в результате вписывал в поле ввода дату в формате ДД.ММ.ГГГГ.

После отправки формы контроллер отправляет дату в вышеуказанном формате в специальный метод в модели, который превращает строковую дату в timestamp.

1
2
3
4
5
6
7
8
9
10
/* ClientsController */
$info = new Clients();
$info->setBirthday($this->request->getPost("birthday"));
/* Clients model */
class Clients{
public function setBirthday($birthday){
$res = strtotime($birthday);
$this->birthday = $res;
}
}

Работает этот код хорошо на следующей конфигурации на PHP5.4 и Phalcon 2.0.3. Однако на PHP5.6 и Phalcon 2.0.13 начинаются проблемы. В результате выполнения функции setBirthday(), в поле birthday записывалось значение false.

В первый раз это произошло у моего коллеги на Fedora с PHP5.6 и Phalcon 2.0.13. Я пытался помочь ему разобраться, но, честно сказать, списывал это на локальные баги окружения, потому что у меня все работало хорошо (Windows 10, OpenServer5.2.2, PHP5.6 и Phalcon 2.0.3). На production сервере все тоже работало хорошо (Phalcon 2.0.3).

Интересная особенность, правда? На 2.0.3 работает нормально, а на 2.0.13 нет. Но об этом далее.

Выявление проблемы

Когда проблема проявилась у меня на машине (PHP5.6, Phalcon 2.0.13) я все-таки решил разобраться в ней. И вот что получилось.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Clients{
public function setBirthday($birthday){
var_dump('---');
var_dump('before strtotime');
var_dump($birthday);
var_dump($this->birthday);
$res = strtotime($birthday);
var_dump($res);
$this->birthday = $res;
var_dump('after strtotime');
var_dump($this->birthday);
var_dump('///');
}
}

Расставил отладочные выводы на каждый чих и посмотрел, что происходит в процессе выполнения функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Начинается выполнение ClientsController->editAction(). 
// Доходит до выполнения $info->setBirthday().
string(18) "before bithday set"
string(3) "—" // начало выполнения функции
string(16) "before strtotime" // вывод перед вызовом функции приведения строки к числу
string(10) "09.09.2016" // вывод из аргумента функции setBirthday
string(0) "" // вывод текущего знаения дня рождения ($this->birthday)
int(1473368400) // результат работы функции приведения строки к числу
string(3) "—" СНОВА начало выполнения функции
string(16) "before strtotime" опять все то же самое
int(1473368400) // приняли уже значение числовое, которое до этого получилось
string(0) "" дата по прежнему пустая
bool(false) // результат работы функции есесьно false,
// потому что число к числу привести не возможно
string(15) "after strtotime"
bool(false) //после выполнения всего такой результат
string(3) "///" // завершилась первая
string(15) "after strtotime" //
bool(false)
string(3) "///" // завершилась вторая

Таким образом, видно, что идет вложенный вызов функции. Контроллер вызывает setBirthday(), она начинает выполняться, потом доходит до установки значения в свойство $this->birthday = $res; и функция начинает выполняться заново, принимает уже числовое значение из strtotime, а потом не может это числовое значение опять привести к числу, потому что strtotime требует строку. Очевидно, что идет какая-то магия, вызов магического метода setBirthday().

Документация Phalcon

В документации Phalcon 3 уже есть блок, описывающий работу магических методов в модели. Но в Phalcon 2 его еще не было.

Не так давно в версии 2.0.11 был закрыт issue, который решает проблему с getters & setters. Таким образом, с версии 2.0.11 они начинают нормально работать, и поэтому на версии 2.0.13 проявлялся баг.

Ну а решение есть в документации: свойства, которые мы меняем через getters/setters должны быть protected.