Наверняка многие, кто достаточно хорошо знает MODX, и уже создавал свои расширенные классы на основе стандартных классов MODX-а, оценил, насколько MODX гибкая и удобная платформа для разработок (сразу оговорюсь, что я не буду здесь рассказывать в подробностях, как создавать свои классы, расширяющие базовые, не мало топиков посвящено теме расширения базовых объектов). Здесь вам и пользовательские документы (ресурсы, CRC), и контроллеры да процессоры свои. Да, это все классно! Но пробовал кто-нибудь создавать свои, расширенные классы пользователей (типа modMyUser extends modUser)?
В чем задача? Допустим, я хочу расширить базовый класс modUser, и так, чтобы в самом объекте $modx->user был именно мой расширенный класс, а не базовый modUser. Зачем? Хотя бы для того, чтобы я мог в любом месте выполнить свою пользовательскую функцию на объекте $modx->user, к примеру $modx->user->getKarma(), и получить карму пользователя. Но плодить таблицы для своих пользователей, а главное самих пользователей (что мы наблюдаем в том же Discuss), не хочется вообще. То есть хочется, чтобы основные данные содержались в таблице modx_users.
Итак, что нам дает сам MODX для реализации желаемого?
Если посмотреть структуру таблицы modx_users, то можно увидеть там колонку class_key, и по умолчанию все записи в ней содержат значение modUser. Что это дает? Это позволяет указать собственный класс для отдельных пользователей. К примеру, я создаю вот такой класс:
<?php require_once MODX_CORE_PATH.'model/modx/moduser.class.php'; class myUser extends modUser { function test(){ return "Some text"; } } ?>
И можно выполнить вот такой код:
$user = $modx->getObject('modUser', $id); print $user->test();
Или даже просто $modx->user->test(); (если пользователь авторизован). Выглядит интересно, не правда ли? Но есть минусы, о которых скажу чуть позже, а пока выясним почему же при вызове объекта 'modUser', мы получаем объект 'myUser'. Для этого посмотрим на метод xPDOObject::load()
public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) { $instance= null; $fromCache= false; if ($className= $xpdo->loadClass($className)) { if (!is_object($criteria)) { $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag); } if (is_object($criteria)) { $row= null; if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) { $row= $xpdo->fromCache($criteria, $className); } if ($row === null || !is_array($row)) { if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) { $row= $rows->fetch(PDO::FETCH_ASSOC); $rows->closeCursor(); } } else { $fromCache= true; } if (!is_array($row)) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true)); } else { $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row); if (is_object($instance)) { if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) { $xpdo->toCache($criteria, $instance, $cacheFlag); if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) { $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag); $xpdo->toCache($pkCriteria, $instance, $cacheFlag); } } if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true)); } } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.'); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className); } return $instance; }
И здесь есть вот такая строка: $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
Именно этот метод инициализирует конечный объект (и наполняет его данными, полученными из базы данных (переменная $row)).
Посмотрим на этот метод внимательней.
public static function _loadInstance(& $xpdo, $className, $criteria, $row) { $rowPrefix= ''; if (is_object($criteria) && $criteria instanceof xPDOQuery) { $alias = $criteria->getAlias(); $actualClass = $criteria->getClass(); } elseif (is_string($criteria) && !empty($criteria)) { $alias = $criteria; $actualClass = $className; } else { $alias = $className; $actualClass= $className; } if (isset ($row["{$alias}_class_key"])) { $actualClass= $row["{$alias}_class_key"]; $rowPrefix= $alias . '_'; } elseif (isset($row["{$className}_class_key"])) { $actualClass= $row["{$className}_class_key"]; $rowPrefix= $className . '_'; } elseif (isset ($row['class_key'])) { $actualClass= $row['class_key']; } $instance= $xpdo->newObject($actualClass); if (is_object($instance) && $instance instanceof xPDOObject) { $pk = $xpdo->getPK($actualClass); if ($pk) { if (is_array($pk)) $pk = reset($pk); if (isset($row["{$alias}_{$pk}"])) { $rowPrefix= $alias . '_'; } elseif ($actualClass !== $className && $actualClass !== $alias && isset($row["{$actualClass}_{$pk}"])) { $rowPrefix= $actualClass . '_'; } elseif ($className !== $alias && isset($row["{$className}_{$pk}"])) { $rowPrefix= $className . '_'; } } elseif (strpos(strtolower(key($row)), strtolower($alias . '_')) === 0) { $rowPrefix= $alias . '_'; } elseif (strpos(strtolower(key($row)), strtolower($className . '_')) === 0) { $rowPrefix= $className . '_'; } $parentClass = $className; $isSubPackage = strpos($className,'.'); if ($isSubPackage !== false) { $parentClass = substr($className,$isSubPackage+1); } if (!$instance instanceof $parentClass) { $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Instantiated a derived class {$actualClass} that is not a subclass of the requested class {$className}"); } $instance->_lazy= $actualClass !== $className ? array_keys($xpdo->getFieldMeta($actualClass)) : array_keys($instance->_fieldMeta); $instance->fromArray($row, $rowPrefix, true, true); $instance->_dirty= array (); $instance->_new= false; } return $instance; }
Здесь самое интересное вот это:
if (isset ($row["{$alias}_class_key"])) { $actualClass= $row["{$alias}_class_key"]; $rowPrefix= $alias . '_'; } elseif (isset($row["{$className}_class_key"])) { $actualClass= $row["{$className}_class_key"]; $rowPrefix= $className . '_'; } elseif (isset ($row['class_key'])) { $actualClass= $row['class_key']; } $instance= $xpdo->newObject($actualClass);
То есть здесь xPDO получает из записи название конечного класса, и создает новый объект (в дальнейшем его наполняя данными $instance->fromArray($row, $rowPrefix, true, true);). Вот этот объект мы и получим на выходе. Собственно этот же механизм реализован и в документах, там тоже в одной таблице данные различных типов объектов с указанием класса в колонке class_key (таблица modx_site_content).
Вот, собственно уже неплохо — свои объекты можно создавать. А чего мне не хватает? А не хватает возможности указывать MODX-у названия текущего класса пользователей. Объясню. Давайте взглянем на метод modX::getAuthenticatedUser()
public function getAuthenticatedUser($contextKey= '') { $user= null; if ($contextKey == '') { if ($this->context !== null) { $contextKey= $this->context->get('key'); } } if ($contextKey && isset ($_SESSION['modx.user.contextTokens'][$contextKey])) { $user= $this->getObject('modUser', intval($_SESSION['modx.user.contextTokens'][$contextKey]), true); if ($user) { $user->getSessionContexts(); } } return $user; }
То есть хотим мы этого, или нет, но первоначально будет выполнена именно инициализация объекта modUser. Это дальше уже будет выполнен метод modUser::load(), и будет подгружен итоговый класс, указанный в колонке class_key (сам по себе класс modUser не имеет метода load(), но он его наследует от родительского класса). А мне хотелось бы иметь возможность указывать какой именно класс использовать по умолчанию, чтобы, к примеру, на уровне метода __construct() выполнить какой-то особый код. Зачем это может понадобиться? Это бы позволило для разных контекстов для одних и тех же пользователей (именно их данных) создавать разные объекты. К примеру, заходя на один домен, у нас обычный пользователь modUser, а на другой домен (где, к примеру, форум), создается другой пользователь, со своим, уникальным функционалом. К примеру, у вас большой разветвленный проект, с большой базой пользователей. Но, к примеру, один контекст — это внешняя социалка, а другой контекст — это закрытый резурс. Если бы можно было для конкретного контекста указать тип пользователя, то объект из одного контекста просто не обладал бы функционалом объектов из другого контекста.
Конечно, все это слишком мудренно, и многим скорее всего покажется абсолютно не нужным, но, дело всего в двух строчках кода в ядре, а прирост в гибкости все-таки существенный. Вот мне это сейчас надо.
Напоследок небольшой пример, как можно было бы тогда рулить, какие объекты в итоге возвращать в $modx->user.
class SocietyUser extends modUser { static function load(xPDO &$xpdo, $className, $criteria, $cacheFlag = true) { $instance= null; $fromCache= false; if ($className= $xpdo->loadClass($className)) { if (!is_object($criteria)) { $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag); } if (is_object($criteria)) { $row= null; if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) { $row= $xpdo->fromCache($criteria, $className); } if ($row === null || !is_array($row)) { if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) { $row= $rows->fetch(PDO::FETCH_ASSOC); $rows->closeCursor(); } } else { $fromCache= true; } if (!is_array($row)) { if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true)); } else { // Перегружаем значение класса $row['SocietyUser_class_key'] = 'SocietyTestUser'; $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row); if (is_object($instance)) { if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) { $xpdo->toCache($criteria, $instance, $cacheFlag); if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) { $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag); $xpdo->toCache($pkCriteria, $instance, $cacheFlag); } } if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true)); } } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.'); } } else { $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className); } return $instance; } function test(){ return "11Sdfsdfsd"; } }