Web разработка. Автоматическое создание методов на PHP

Владимир | | PHP.

Заставка к статье Автоматическое создание методов на PHP

Если вы занимались объектно-ориентированным программированием, то, безусловно, знаете, что в реальных приложениях количество методов классов может быть довольно большим. Причем во многих из этих методов код попросту повторяется.

Типичный пример – методы установки и чтения свойств (или, если использовать английскую терминологию, getters and setters). Названия этих методов обычно совпадают с названиями свойств класса, только к ним добавляется приставка get или set.

Примечание. Свойства и методы класса – это переменные и функции, объявленные в нем.

Написать такие методы несложно. По-идее, они должны просто возвращать/присваивать значение свойствам. Но на практике, при присваивании нужно еще и проверять полученное значение.

Теперь представьте, что вы пишите класс, который должен хранить данные одной записи из таблицы в базе данных. А в этой таблице 20 полей. Т.е. нужно написать 40 методов. Они, конечно, простые, тем не менее, каждый все равно должен быть протестирован, а это приличный кусок работы.

В этой статье я покажу способ, позволяющий автоматизировать написание свойств.

Идея заключается в использовании метода __call(). Если этот метод объявлен в классе, то вызовы всех несуществующих методов будут переданы ему. Первый параметр __call будет содержать имя вызванного метода, а второй – массив с переданными параметрами.

Рассмотрим небольшой пример. Допустим, у нас есть класс.

class UserData {
    private $name;
    private $nic;
    private $email;

    //конструктор
    function UserData() {
    }

    function __call($method, $val = null) {
    }
}

Теперь мы можем использовать этот класс.

$uData = new UserData();
$uData->setName("my_name");

Этот код создает объект класса UserData и вызывает его метод setName. Такого метода в классе нет. Вместо него php вызовет метод __call. Его параметры будут равны:
$method – “setName”, а $val[0] – “my_name”.

Таким образом, мы можем вызывать методы, которые на самом деле не объявлены.
Если с теорией все понятно, напишем метод, который будет обрабатывать вызовы типа:
get<PropertyName> и set<PropertyName> (PropertyName может быть любым именем свойства класса).

function __call($method, $val = null) {
    $vars = get_class_vars("UserData");
    $operation = substr($method, 0, 3);
    $property = strtolower(substr($method, 3));
    if (in_array($property, array_keys($vars))) {
        switch ($operation) {
            case "get": {
                return $this->$property;
            }
            case "set": {
                if ($this->validate($property, $val) === TRUE) {
                    $this->$property = $val[0];
                }
                else {
                    //Тут можно использовать другой метод сообщения об ошибке
                    trigger_error("Validation not pass. Метод: ".$method, E_USER_ERROR);
                }
                return;
            }
            //если указана неподдерживаемая операция
            default: {
                trigger_error("Unsupported method", E_USER_ERROR);
            }
        }
    }
    //если указано несуществующее имя свойства
    else {
        trigger_error("Unsupported method", E_USER_ERROR);
    }
}

Рассмотрим этот метод подробнее.

В начале мы получаем перечень свойств класса (строка 2). После этого, по имени метода определяем название операции и свойства (строки 3 и 4).

Затем, если указанное свойство существует (строка 5), выполняем заданную операцию. Т.е. либо возвращаем значение свойства (строки 7-9), либо присваиваем ему новое значение (10-19).

Во всех остальных случаях мы генерируем ошибку.

Как видите, метод содержит 30 строк (если убрать комментарии будет 27). При этом он по-сути создает методы get и set для каждого свойства, объявленного в классе.

Проверка значений

Если вы внимательно смотрели код метода __call(), то, наверное, заметили вызов метода validate() (строка 11).

Этот метод выполняет проверку полученных значений, до присвоения их свойствам. Естественно, использовать одни и те же проверки для всех свойств нельзя, поэтому мы создадим специальный массив с правилами.

Примечание. Идею массива с правилами (и их форму записи) я позаимствовал из библиотеки validation, входящей в состав фреймворка CodeIgniter.

Массив имеет имя $rules. Ключи массива должны совпадать с именами свойств, а значения – перечень правил, разделенных символом “|”.

В качестве правил можно использовать имена стандартных функций php, которые возвращают true или false (например, is_numeric, is_string и т.п.).

Кроме того, можно написать свою собственную функцию для проверки данных. Если вы указываете эту функцию в перечне правил к ее имени нужно добавить приставку «callback_«. Функция всегда должна возвращать true или false.

Например:

private $rules = array("email" => "callback_isValidEmail", "name" => "is_string");

Рассмотрим метод validate подробнее.

function validate($property, $val) {
    if (!isset($val[0])) {
        return FALSE;
    }
    $validationRes = TRUE;
    if (array_key_exists($property, $this->rules)) {
        $validationRes = FALSE;
        $rules_array = explode("|", $this->rules[$property]);
        foreach ($rules_array as $curRule) {
            if (substr($curRule, 0, 9) === "callback_") {
                $m = substr($curRule, 9);
                if (call_user_func(array("UserData", $m), $val[0]) === TRUE) {
                    $validationRes = TRUE;
                }
                else {
                    $validationRes = FALSE;
                    break;
                }
            }
            else {
                if(call_user_func($curRule, $val[0]) === TRUE) {
                    $validationRes = TRUE;
                }
                else {
                    $validationRes = FALSE;
                    break;
                }
            }
        }
    }
    return $validationRes;
}

В строках 2-4 мы проверяем установлен ли параметр метода.

Примечание. Метод set... по определению может получить только один параметр, и он всегда будет находиться в нулевом элементе массива $val.

После этого, мы проверяем, заданы ли правила для данного свойства. Если они заданы, то с помощью call_user_func(...) вызываем перечисленные функции.

Предварительно мы проверяем, содержит ли название функции приставку callback_ (строка 10). Если приставка найдена, убираем ее и вызываем метод данного класса (в этом примере класс должен содержать метод isValidEmail($value), который будет выполнять проверку формата адреса электронной почты).

Если все проверки прошли успешно, то метод validate вернет TRUE и значение будет присвоено свойству класса.

Заключение

Как видите, два метода позволяют существенно сократить объем работы (если, конечно, у ваш класс содержит не 2-3 свойства).

И, самое главное, этот подход дает возможность изменять свойства класса и правила проверки, не меняя код методов.

До встречи!

  • Sam

    А как же дополнение кода?

    • В смысле? Не понял вопрос.

      • Sam

        Я и многие другие пользуют IDE вроде PDT или Zend Studio. Там есть такая замечательная штука как дополнение кода, а именно методов класса. Без неё на написание контроллера уйдёт больше времени, чем на описание всех методов руками.

        • Дошло, сам использую PDT.
          Дополнение кода вещь, конечно, очень полезная, но при этом все равно каждый метод пишется (вставляется) отдельно.
          А я хотел убрать дублирование кода вообще, т.е. свести все в один метод.
          Если будет найдена ошибка, придется исправлять только один метод.

  • Sam

    А как же дополнение кода?

    • В смысле? Не понял вопрос.

      • Sam

        Я и многие другие пользуют IDE вроде PDT или Zend Studio. Там есть такая замечательная штука как дополнение кода, а именно методов класса. Без неё на написание контроллера уйдёт больше времени, чем на описание всех методов руками.

        • Дошло, сам использую PDT.
          Дополнение кода вещь, конечно, очень полезная, но при этом все равно каждый метод пишется (вставляется) отдельно.
          А я хотел убрать дублирование кода вообще, т.е. свести все в один метод.
          Если будет найдена ошибка, придется исправлять только один метод.

  • А зачем изменять private свойства? Не логичнее ли их тогда просто объявить как public и использовать классическое объявление свойства:

    $myClass->myobject = true; ?

    • В этом случае нельзя выполнить автоматическую проверку значения при установке свойства.

      • Т.е. все это ради проверки корректности задаваемых значений?

        Способ интересный, не думал о нем. Но логичнее использовать __get() и __set(). Не вижу смысла изобретать велосипед

        • Не столько ради проверки, сколько ради сокращения кода и упрощения его поддержки (внесения изменений).

          Насчет __get() и __set() согласен, в данном примере их использовать удобнее, но я хотел показать более универсальный вариант, который подойдет для методов с любыми названиями. Просто get/set самый распространенный пример.

  • А зачем изменять private свойства? Не логичнее ли их тогда просто объявить как public и использовать классическое объявление свойства:

    $myClass->myobject = true; ?

    • В этом случае нельзя выполнить автоматическую проверку значения при установке свойства.

      • Т.е. все это ради проверки корректности задаваемых значений?

        Способ интересный, не думал о нем. Но логичнее использовать __get() и __set(). Не вижу смысла изобретать велосипед

        • Не столько ради проверки, сколько ради сокращения кода и упрощения его поддержки (внесения изменений).

          Насчет __get() и __set() согласен, в данном примере их использовать удобнее, но я хотел показать более универсальный вариант, который подойдет для методов с любыми названиями. Просто get/set самый распространенный пример.

  • Ali

    Не совсем согласен по нескольким пунктам.
    Зачем морочить голову с переопределением __call (его можно использовать для более полезных вещей), если есть другая магия? Я имею ввиду __get() и __set(). Эти методы как раз для таких задач и предназначены. И валидацию устроить можно.
    Еще одна проблема — скорость выполнения, любая магия, к сожалению, ее снижает…
    А вообще, чтобы не морочиться с геттерами и сеттерами вручную, можно использовать фреймворки и автогенерацию кода по модели (тот же symfony с успехом это делает). А уж рефакторинг как проходит на ура…

    • Да, __get() и __set() здесь подходят лучше, но хотелось показать общий пример с разбором названия метода, определением параметров и т.д.

      А вот автогенерация вариант хороший. Но это отдельная тема 🙂

      Насчет скорости полностью согласен. К сожалению, ничего «на шару» не дается.

  • Ali

    Не совсем согласен по нескольким пунктам.
    Зачем морочить голову с переопределением __call (его можно использовать для более полезных вещей), если есть другая магия? Я имею ввиду __get() и __set(). Эти методы как раз для таких задач и предназначены. И валидацию устроить можно.
    Еще одна проблема — скорость выполнения, любая магия, к сожалению, ее снижает…
    А вообще, чтобы не морочиться с геттерами и сеттерами вручную, можно использовать фреймворки и автогенерацию кода по модели (тот же symfony с успехом это делает). А уж рефакторинг как проходит на ура…

    • Да, __get() и __set() здесь подходят лучше, но хотелось показать общий пример с разбором названия метода, определением параметров и т.д.

      А вот автогенерация вариант хороший. Но это отдельная тема 🙂

      Насчет скорости полностью согласен. К сожалению, ничего «на шару» не дается.

  • Всё на Java становится похоже. Вообще спорный вопрос — если валидация будет иметь кучу своих типов.. не лучше ли иметь отдельно ясно выраженные set-функции чем одну с большим switch ?

    • Ответ на этот вопрос зависит от ваших предпочтений. Оба подхода имеют и достоинства и недостатки.

  • Всё на Java становится похоже. Вообще спорный вопрос — если валидация будет иметь кучу своих типов.. не лучше ли иметь отдельно ясно выраженные set-функции чем одну с большим switch ?

    • Ответ на этот вопрос зависит от ваших предпочтений. Оба подхода имеют и достоинства и недостатки.