AdSense - шапка

среда, 30 ноября 2011 г.

PHP. Разработка классов - моделей с использованием магических методов __set и __get

Сегодня я покажу небольшой пример разработки классов - моделей.

Модель - это класс, который используется для хранения данных о каких-либо сущностях, например, о пользователях, статьях, платежах и т.д.

Разработка классов - моделей с помощью волшебных методов __set, __get, __isset, __unset часто используется во многих веб-приложениях и фреймворках, например, в известном фреймворке Yii.

Для простоты, будем использовать только первые два волшебных метода. С их помощью мы сможем написать сеттеры и геттеры для доступа к данным, а сами данные будем хранить в массиве.


Пример абстрактной модели
abstract class Model
{
    private $data = array();

    /**
     * Устанавливает значение свойства.
     *
     * @param string $propertyName
     * @param mixed $value
     */
    public function __set($propertyName, $value)
    {
        $this->validate($propertyName, $value);
        $this->data[$propertyName] = $value;
    }

    /**
     * Возвращает значение свойства.
     *
     * @param string $propertyName
     */
    public function __get($propertyName)
    {
        if (!array_key_exists($propertyName, $this->data))
        {
            throw new Excpetion("Свойство '$propertyName' не существует.");
        }
        return $this->data[$propertyName];
    }

    /**
     * Возвращает схему валидации свойств.
     * @return IValidator[]
     */
    abstract protected function getValidationSchema();

    /**
     * Производит валидацию значения свойства.
     *
     * @param string $propertyName
     * @param mixed $value
     */
    private function validate($propertyName, $value)
    {
        $validationSchema = $this->getValidationSchema();
        if (empty($validationSchema))
        {
            return;
        }

        $validator = (isset($validationSchema[$propertyName])) ? $validationSchema[$propertyName] : null;
        if (empty($validator))
        {
            throw new Exception("Недопустимое свойство '$propertyName'");
        }
        if (!$validator->validate($value))
        {
            throw new Exception("Невалидное значение " . var_export($value) . " для свойства '$propertyName'");
        }
    }
}
Если унаследоваться от такой модели, то можно выполнять следующий код:
$user->name = 'Adam';
$user->age = 10;
Но для работы класса Model требуется разработать несколько валидаторов - они будут проверять корректность устанавливаемого значения. Например, чтобы имя пользователя было строкой, потому что плохо, когда человека зовут 0.5, например =)
/**
 * Интерфейс валидатора.
 */
interface IValidator
{
 /**
  * Производит валидацию переданного значения.
  *
  * @param mixed $value
  * @return bool
  */
 function Validate($value);
}

/**
 * Валидатор строк.
 */
class StringValidator implements IValidator
{
 private $maxLength = null;

 public function __construct($maxLength = null)
 {
  $this->maxLength = $maxLength;
 }

 public function Validate($value)
 {
  $valid = is_string($value);
  if ($valid && ($this->maxLength !== null))
  {
   $valid = (strlen($value) <= $this->maxLength);
  }
  return $valid;
 }
}

/**
 * Валидатор чисел.
 */
class IntegerValidator implements IValidator
{
 public function Validate($value)
 {
  return is_int($value);
 }
}

/**
 * Валидатор пользователя.
 */
class UserValidator implements IValidator
{
 public function Validate($value)
 {
  return ($value instanceof User);
 }
}
Определим два класса - модели: User - модель пользователя, Article - модель статьи.
class Article extends Model
{
 protected function getValidationSchema()
 {
  return array(
   "author"       => new UserValidator(),
   "title"        => new StringValidator(30),
   "content"      => new StringValidator(),
  );
 }
}

class User extends Model
{
 protected function getValidationSchema()
 {
  return array(
   "name"     => new StringValidator(30),
   "age"      => new IntegerValidator()
  );
 }
}
Теперь мы можем создать парочку моделей:
$user = new User();
$user->name = "test name";
$user->age  = 10;

$article = new Article();
$article->author     = $user;
$article->title      = "test article";
$article->content    = "test content";
При этом, будет происходить автоматическая валидация данных и мы можем положиться на корректность моделей.

Теперь мы можем создавать множество классов моделей, не создавая каждый раз публичные свойства для доступа к данным, а просто задав валидаторы.

Приведенный выше класс Model можно расширить:
  1. Добавить инстанциирование полей модели значениями по-умолчанию.
  2. Добавить возможность конвертировать модель в массив, JSON, XML и т.д.
Можете заняться этим самостоятельно в собственных проектах =)

4 комментария:

  1. При использовании данного подхода не будет работать auto completion в IDE. Придется держать в памяти все наборы атрибутов моделей.

    Когда в проекте много подобных моделей с неизвестным набором полей, это может создавать серьезные неудобства.

    Для нового человека в команде это может стать критичным. Порог вхождения резко возрастет.

    ОтветитьУдалить
  2. Интересное замечание. Ответ продублирую в статью.
    В IDE всё будет замечательно работать =) Подсмотрел в Yii. У класса можно добавить в комментариях следующее:
    /**
    * @property string $name
    * @property integer $age
    */
    Тогда auto completion будет работать и набор полей всем будет известен.

    ОтветитьУдалить
  3. Не забывайте про рефакторинг - при переименовании поля (а такое случается), найти все использования свойства становится проблематично (= трудоемко).

    ОтветитьУдалить
  4. Это не проблема именно данного подхода - это проблема IDE в большей степени. Если вы, например, захотите переименовать функцию - то будет точно также проблематично найти все её использования.

    В отношении рефакторинга очень нравится как сделано в Visua Studio 2010 для C#. Мечтаю, что когда нибудь такое будет и для PHP =)

    ОтветитьУдалить