Пишу топик с описанием новейших технологий, корни которых берут свое начало еще вот в этом топике, написанном более двух лет назад. Решил его перенести сюда. Почитайте пока, а я статью свою допишу. Она довольно интересная :)
Один из самых главных барьеров в переходе с MODX Evo на MODX Revo — это xPDO. Многим выносит мозг тот факт, что надо создавать физические файлы с какими-то классами, генерировать схему и много еще каких-то танцев с бубнами. «Невозможность» работать в полной мере с базой данных отпугивает очень многих, и многие продолжают разрабатывать на Эво, просто потому что там «проще», хотя и с соблазном смотрят на всякие плюшки Ревы, типа пакетов, источников файлов и т.п.
Но ответьте мне на такой вопрос: «Вы родились со знаниями того, как работать с mysql? Все сразу освоили mysql_connect(), mysql_select_db(), mysql_query() и т.д.и т.п.?» Согласитесь, что все это так же приходилось осваивать, и совсем не за один день.
Я сейчас приведу совсем небольшой, но очень и очень хитрый код (результат моих последних исследований xPDO и продолжение позавчерашней темы), а под катом вы узнаете очень много нового, и возможно кому-то работа с xPDO покажется еще проще, чем с mysql-функциями и библиотеками.
$modx->map['page'] = array (
'table' => 'site_content',
'fields' =>
array (
'id' => '',
'pagetitle' => '',
'content' => '',
),
);
class page extends xPDOObject{}
class page_mysql extends page{}
$o=$modx->getObject('page', array(
'id' => 1
));
Что здесь происходит?
А происходит следующее: мы быстренько и на лету создали свой объект для взаимодействия с базой данных ( в частности с таблицей site_content (вместе с префиксом modx_site_content)), и извлекли из нее запись с id=1 (выполнили запрос).
Далее можно, к примеру, получить значения объекта. К примеру так:
$content = $o->get('content');
Или так (сразу все данные):
$data_array = $o->toArray();
Заметка: имя таблицы указано в описании объекта
'table' => 'site_content',
Как это работает? (вкратце)
- Мы создали классы page и page_mysql, расширяющие объект xPDOObject (так как именно объекты xPDOObject имеют необходимый функционал для взаимодействия с базой данных, типа load(),save(), remove() и т.п.)
- Мы добавили описание своего объекта в окружение xPDO ($modx->map['page']) (не забывает, что MODx — это расширение класса xPDO).
- Получили объект (выполнили запрос)$o=$modx->getObject('page', array( 'id' => 1 ));
Что это дает?
Такой подход позволяет выполнять взаимодействие с базой данных вообще из любого положения, будь то сниппет, плагин или даже во внешнем файле. Вот такой код работает:
// Bla-Bla-Bla include MODx class
$modx= new modX();
$modx->map['page'] = array (
'table' => 'site_content',
'fields' =>
array (
'id' => '',
'pagetitle' => '',
'content' => '',
),
);
Читай: самый быстрый коннектор с MODX-окружением и без лишних инициализаций классов-реквестеров-респонсеров, сессии и т.п. Можно еще облегчить, если напрямую подсосать конфиги и инициализировать не MODx класс, а xPDO.
Плюс к этому можно четко управлять какие колонки извлекать, а какие нет, и не бояться за повторные запросы (читаем про это здесь).
И вообще еще много чего дает, о чем я прям сейчас не буду писать, но в дальнейшем буду время от времени выкладывать примеры.
А почему не писать чистые SQL-запросы и не выполнять их через $modx->prepare($sql)?
Здесь несколько причин.
1. Префиксы таблиц в базе данных. По умолчанию они modx_, но не редкость, когда и отличаются. Даже если вы перед каждым запросом будет получать имя таблицы через API MODX-а, то это как минимум не удобно.
2. Написание чистых SQL-запросов так же требуют знаний, и не малых. В приведенном же случае достаточно просто знать структуру таблицы (какие есть колонки).
3. Просто выборка — это еще ладно, не сложно (select * from table;). А что вы будете делать, если вам надо обновить 28 колонок за раз? Могу сказать точно, что написание такого SQL-запроса так же займет не мало времени.
Есть еще причины, но этого, думаю, достаточно.
А почему сразу 2 класса надо, а не один?
Один класс — чисто базовый, содержащий дополнительный функционал. Второй класс — для своего типа базы данных, чтобы логику взаимодействия с БД можно было разделить. То есть если это mysql, то класс будет classname_mysql, если MSSQL SERVER, то classname_sqlsrv.
Более подробно с примерами.
Приведенный выше пример — это простейший вариант, позволяющий только делать выборки из БД. Для более полного взаимодействия с базой требуется описание колонок таблицы (я не нашел документации с описанием мета-данных xPDO-объектов, потому напишу при аказии мануал, а пока по примерам и копаем класс xPDOObject).
В итоге, если мы хотим, чтобы можно было сохранять данные объектов в БД, то нам понадобится описание колонок. К слову, при попытке сохранения объекта xPDO будет писать запись в таблицу, только она не будет содержать значений.
Итак, дополним наш класс
$modx->map['page'] = array (
'table' => 'site_content',
'fields' =>
array (
'id' => '',
'pagetitle' => '',
'content' => '',
),
'fieldMeta' =>
array (
'id' =>
array (
'dbtype' => 'int',
'precision' => '10',
'attributes' => 'unsigned',
'phptype' => 'integer',
'null' => false,
'index' => 'pk',
'generated' => 'native',
),
'pagetitle' =>
array (
'dbtype' => 'varchar',
'precision' => '255',
'phptype' => 'string',
'null' => false,
'default' => '',
'index' => 'fulltext',
'indexgrp' => 'content_ft_idx',
),
'content' =>
array (
'dbtype' => 'mediumtext',
'phptype' => 'string',
'index' => 'fulltext',
'indexgrp' => 'content_ft_idx',
),
),
);
class page extends xPDOObject{}
class page_mysql extends page{}
$o=$modx->newObject('page', array(
'pagetitle' => 'new pagetitle',
'content' => 'some content',
));
$o->save();
Здесь мы добавили массив-описание с мета-данными колонок. Зачем они нужны? Они позволяют определить xPDO-объекту какого типа данные хранятся на стороне базы данных, а какие типы на стороне php. Рассмотрим на примере описания колонки id:
'id' =>
array (
'dbtype' => 'int', // Тип данных настороне базы (число)
'precision' => '10', // Длина (10 цифр)
'attributes' => 'unsigned', // только положительное
'phptype' => 'integer', // Тип на стороне PHP - число
'null' => false, // Может ли быть нулевым*
'index' => 'pk', // индекс Primary Key
'generated' => 'native', // Флаг, что генерируемое. То есть если не указано и не нулевое, то будет надеяться на базу данных, иначе ругаться будет
),
Вот когда колонка четко описана, тогда xPDO знает как работать с данной таблицей, и можно сохранять данные в базу методом ->save();
Кстати, тут есть хитрость: дело в том, что ООП никто не отменял, и можно переопределять не только xPDOObject, но и его дочерние классы, к примеру xPDOSimpleObject. Что это нам дает? В xPDOSimpleObject уже описана колонка id (и ничего более кроме нее), и поэтому мы можем выкинуть ее из нашего описания. Вот описание объекта xPDOSimpleObject:
<?php
/*
* Copyright 2010-2012 by MODX, LLC.
*
* This file is part of xPDO.
*
* xPDO is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
* Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Metadata map for the xPDOSimpleObject class.
*
* Provides an integer primary key column which uses MySQL's native
* auto_increment primary key generation facilities.
*
* @see xPDOSimpleObject
* @package xpdo
* @subpackage om.mysql
*/
$xpdo_meta_map = array(
'xPDOSimpleObject' => array(
'table' => null,
'fields' => array(
'id' => null,
),
'fieldMeta' => array(
'id' => array(
'dbtype' => 'INTEGER',
'phptype' => 'integer',
'null' => false,
'index' => 'pk',
'generated' => 'native',
'attributes' => 'unsigned',
)
),
'indexes' => array(
'PRIMARY' =>
array(
'alias' => 'PRIMARY',
'primary' => true,
'unique' => true,
'type' => 'BTREE',
'columns' =>
array(
'id' =>
array(
'length' => '',
'collation' => 'A',
'null' => false,
),
),
)
)
)
);
К слову, находится он в файле core/xpdo/om/mysql/xpdosimpleobject.map.inc.php, а не в model/modx/… Это вам гарантирует, что ваши объекты, расширяющие этот класс, будут работать даже за пределами MODX на чистом xPDO. Из MODX-объектов минимум 28 штук расширяют этот класс, дабы не плодить описание колонки id.
Обновим наш код, удалив описание колонки id.
<?php
print '<pre>';
$modx->map['page'] = array (
'table' => 'site_content',
'fields' =>
array (
'pagetitle' => '',
'content' => '',
),
'fieldMeta' =>
array (
'pagetitle' =>
array (
'dbtype' => 'varchar',
'precision' => '255',
'phptype' => 'string',
'null' => false,
'default' => '',
'index' => 'fulltext',
'indexgrp' => 'content_ft_idx',
),
'content' =>
array (
'dbtype' => 'mediumtext',
'phptype' => 'string',
'index' => 'fulltext',
'indexgrp' => 'content_ft_idx',
),
),
);
class page extends xPDOSimpleObject{}
class page_mysql extends page{}
$o=$modx->getObject('page', 1);
print_r($o->toArray());
Вот, уже компактней. Многие конечно могут посетовать на то, что описывая так объекты в своих сниппетах, они получатся громозскими. Но ведь и здесь можно играться. Создать несколько базовых элементарных классов, воткнуть их в один плагин, и расширять нужные. Ведь расширять и 10 классов можно. Кстати, несколько классов, воткнутых в плагин, будет быстрее работать, чем рассовывать их по файлам по классической модели, так как по классике получается на каждый объект по 3 файла чтение, а плагин — один. Читай: еще момент для оптимизации, хоть и не значительной.
В оставшихся колонках особо описание не отличается, только что типы данных другие, но на последок есть у меня еще фишка про запас:-)
Давайте посмотрим на код метода xPDOObject::save() (правда он не маленький)
public function save($cacheFlag= null) {
if ($this->isLazy()) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Attempt to save lazy object: ' . print_r($this->toArray('', true), 1));
return false;
}
$result= true;
$sql= '';
$pk= $this->getPrimaryKey();
$pkn= $this->getPK();
$pkGenerated= false;
if ($this->isNew()) {
$this->setDirty();
}
if ($this->getOption(xPDO::OPT_VALIDATE_ON_SAVE)) {
if (!$this->validate()) {
return false;
}
}
if (!$this->xpdo->getConnection(array(xPDO::OPT_CONN_MUTABLE => true))) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not get connection for writing data", '', __METHOD__, __FILE__, __LINE__);
return false;
}
$this->_saveRelatedObjects();
if (!empty ($this->_dirty)) {
$cols= array ();
$bindings= array ();
$updateSql= array ();
foreach (array_keys($this->_dirty) as $_k) {
if (!array_key_exists($_k, $this->_fieldMeta)) {
continue;
}
if (isset($this->_fieldMeta[$_k]['generated'])) {
if (!$this->_new || !isset($this->_fields[$_k]) || empty($this->_fields[$_k])) {
$pkGenerated= true;
continue;
}
}
if ($this->_fieldMeta[$_k]['phptype'] === 'password') {
$this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password');
}
$fieldType= PDO::PARAM_STR;
$fieldValue= $this->_fields[$_k];
if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') {
$this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
continue;
}
elseif ($fieldValue === null || $fieldValue === 'NULL') {
if ($this->_new) continue;
$fieldType= PDO::PARAM_NULL;
$fieldValue= null;
}
elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('timestamp', 'datetime')) && in_array($fieldValue, $this->xpdo->driver->_currentTimestamps, true)) {
$this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
continue;
}
elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('date')) && in_array($fieldValue, $this->xpdo->driver->_currentDates, true)) {
$this->_fields[$_k]= strftime('%Y-%m-%d');
continue;
}
elseif ($this->_fieldMeta[$_k]['phptype'] == 'timestamp' && preg_match('/int/i', $this->_fieldMeta[$_k]['dbtype'])) {
$fieldType= PDO::PARAM_INT;
}
elseif (!in_array($this->_fieldMeta[$_k]['phptype'], array ('string','password','datetime','timestamp','date','time','array','json'))) {
$fieldType= PDO::PARAM_INT;
}
if ($this->_new) {
$cols[$_k]= $this->xpdo->escape($_k);
$bindings[":{$_k}"]['value']= $fieldValue;
$bindings[":{$_k}"]['type']= $fieldType;
} else {
$bindings[":{$_k}"]['value']= $fieldValue;
$bindings[":{$_k}"]['type']= $fieldType;
$updateSql[]= $this->xpdo->escape($_k) . " = :{$_k}";
}
}
if ($this->_new) {
$sql= "INSERT INTO {$this->_table} (" . implode(', ', array_values($cols)) . ") VALUES (" . implode(', ', array_keys($bindings)) . ")";
} else {
if ($pk && $pkn) {
if (is_array($pkn)) {
$iteration= 0;
$where= '';
foreach ($pkn as $k => $v) {
$vt= PDO::PARAM_INT;
if ($this->_fieldMeta[$k]['phptype'] == 'string') {
$vt= PDO::PARAM_STR;
}
if ($iteration) {
$where .= " AND ";
}
$where .= $this->xpdo->escape($k) . " = :{$k}";
$bindings[":{$k}"]['value']= $this->_fields[$k];
$bindings[":{$k}"]['type']= $vt;
$iteration++;
}
} else {
$pkn= $this->getPK();
$pkt= PDO::PARAM_INT;
if ($this->_fieldMeta[$pkn]['phptype'] == 'string') {
$pkt= PDO::PARAM_STR;
}
$bindings[":{$pkn}"]['value']= $pk;
$bindings[":{$pkn}"]['type']= $pkt;
$where= $this->xpdo->escape($pkn) . ' = :' . $pkn;
}
if (!empty ($updateSql)) {
$sql= "UPDATE {$this->_table} SET " . implode(',', $updateSql) . " WHERE {$where}";
}
}
}
if (!empty ($sql) && $criteria= new xPDOCriteria($this->xpdo, $sql)) {
if ($criteria->prepare()) {
if (!empty ($bindings)) {
$criteria->bind($bindings, true, false);
}
if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Executing SQL:\n{$sql}\nwith bindings:\n" . print_r($bindings, true));
if (!$result= $criteria->stmt->execute()) {
$errorInfo= $criteria->stmt->errorInfo();
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n" . $criteria->toSQL() . "\n" . print_r($errorInfo, true));
if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $this->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
if ($this->xpdo->getManager() && $this->xpdo->manager->createObjectContainer($this->_class) === true) {
if (!$result= $criteria->stmt->execute()) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n{$sql}\n");
}
} else {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $this->xpdo->errorCode() . " attempting to create object container for class {$this->_class}:\n" . print_r($this->xpdo->errorInfo(), true));
}
}
}
} else {
$result= false;
}
if ($result) {
if ($pkn && !$pk) {
if ($pkGenerated) {
$this->_fields[$this->getPK()]= $this->xpdo->lastInsertId();
}
$pk= $this->getPrimaryKey();
}
if ($pk || !$this->getPK()) {
$this->_dirty= array();
$this->_validated= array();
$this->_new= false;
}
$callback = $this->getOption(xPDO::OPT_CALLBACK_ON_SAVE);
if ($callback && is_callable($callback)) {
call_user_func($callback, array('className' => $this->_class, 'criteria' => $criteria, 'object' => $this));
}
if ($this->xpdo->_cacheEnabled && $pk && ($cacheFlag || ($cacheFlag === null && $this->_cacheFlag))) {
$cacheKey= $this->xpdo->newQuery($this->_class, $pk, $cacheFlag);
if (is_bool($cacheFlag)) {
$expires= 0;
} else {
$expires= intval($cacheFlag);
}
$this->xpdo->toCache($cacheKey, $this, $expires, array('modified' => true));
}
}
}
}
$this->_saveRelatedObjects();
if ($result) {
$this->_dirty= array ();
$this->_validated= array ();
}
return $result;
}
Что здесь самое интересное? А интересное начинается с этой строчки:
foreach (array_keys($this->_dirty) as $_k) {
Смотрим, к примеру, на это:
if ($this->_fieldMeta[$_k]['phptype'] === 'password') {
$this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password');
}
То есть, если в описании колонки есть 'phptype'=>'password', то значение этой переменной автоматически будет закодировано методом $this->encode() Посмотрим на этот метод.
public function encode($source, $type= 'md5') {
if (!is_string($source) || empty ($source)) {
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::encode() -- Attempt to encode source data that is not a string (or is empty); encoding skipped.');
return $source;
}
switch ($type) {
case 'password':
case 'md5':
$encoded= md5($source);
break;
default :
$encoded= $source;
$this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::encode() -- Attempt to encode source data using an unsupported encoding algorithm ({$type}).");
break;
}
return $encoded;
}
То есть если тип — password или md5, то значение будет закодировано в md5.
И не забывайте, что это ООП, то есть этот метод можно переопределить.
Или вот это:
if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') {
$this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
continue;
}
То есть если тип — datetime или timestamp и указан атрибут 'ON UPDATE CURRENT_TIMESTAMP', то при сохранении объекта xPDO автоматически будет обновлять значение на текущее время.
К слову, это описание есть в класс modSystemSetting, и мы всегда видим время последнего обновления. При этом исключается человеческий фактор, что кто-то забудет обновить это поле. То есть даже если вы в своем сниппете сделаете так:
$o = $modx->getObject('modSystemSetting','site_name');
$o->set('value', 'New site name');
$o->save();
, время изменения записи зафиксируется.
В общем там еще многое всего очень и очень интересного, и я буду постепенно выкладывать новые материалы.
P.S. Если кто-то все еще считает, то xPDO сложно и вообще не заслуживает внимания, тот — не я.
БлагоДарю тебя друг за такой интереснейший пост!!!
Ближе к середине был уверен, что выражу благодарность в комментариях обязательно. :)
Всегда пожалуйста! :)
Хорошая статья, спасибо за предоставленный материал
Пожалуйста.
Спасибо большое, очень полезная статья)
А как выбрать из таблицы в БД массив строк?
Например, я пишу в сниппете:
<?php $modx->map['page'] = array ( 'table' => 'shopkeeper3_orders', 'fields' => array ( 'id' => '', 'price' => '', 'date' => '', 'sentdate' => '', 'email' => '', 'delivery' => '', 'payment' => '', 'status' => '', ), ); class page extends xPDOObject{} class page_mysql extends page{} $o=$modx->getObject('page', array( 'id' => $modx->getUser()->get("id") )); return $o->get('status');
Возвращается результат только первой записи в таблице, ассоциированной с данным пользователем
Всё, решил вопрос через getCollection)