Пишу топик с описанием новейших технологий, корни которых берут свое начало еще вот в этом топике, написанном более двух лет назад. Решил его перенести сюда. Почитайте пока, а я статью свою допишу. Она довольно интересная :)
Один из самых главных барьеров в переходе с MODX Evo на MODX Revo — это xPDO. Многим выносит мозг тот факт, что надо создавать физические файлы с какими-то классами, генерировать схему и много еще каких-то танцев с бубнами. «Невозможность» работать в полной мере с базой данных отпугивает очень многих, и многие продолжают разрабатывать на Эво, просто потому что там «проще», хотя и с соблазном смотрят на всякие плюшки Ревы, типа пакетов, источников файлов и т.п.
Но ответьте мне на такой вопрос: «Вы родились со знаниями того, как работать с mysql? Все сразу освоили mysql_connect(), mysql_select_db(), mysql_query() и т.д.и т.п.?» Согласитесь, что все это так же приходилось осваивать, и совсем не за один день.
Я сейчас приведу совсем небольшой, но очень и очень хитрый код (результат моих последних исследований xPDO и продолжение позавчерашней темы), а под катом вы узнаете очень много нового, и возможно кому-то работа с xPDO покажется еще проще, чем с mysql-функциями и библиотеками.
А происходит следующее: мы быстренько и на лету создали свой объект для взаимодействия с базой данных ( в частности с таблицей 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=newmodX();$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 будет писать запись в таблицу, только она не будет содержать значений.
Здесь мы добавили массив-описание с мета-данными колонок. Зачем они нужны? Они позволяют определить 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.
Вот, уже компактней. Многие конечно могут посетовать на то, что описывая так объекты в своих сниппетах, они получатся громозскими. Но ведь и здесь можно играться. Создать несколько базовых элементарных классов, воткнуть их в один плагин, и расширять нужные. Ведь расширять и 10 классов можно. Кстати, несколько классов, воткнутых в плагин, будет быстрее работать, чем рассовывать их по файлам по классической модели, так как по классике получается на каждый объект по 3 файла чтение, а плагин — один. Читай: еще момент для оптимизации, хоть и не значительной.
В оставшихся колонках особо описание не отличается, только что типы данных другие, но на последок есть у меня еще фишка про запас:-)
Давайте посмотрим на код метода xPDOObject::save() (правда он не маленький)
publicfunctionsave($cacheFlag=null){if($this->isLazy()){$this->xpdo->log(xPDO::LOG_LEVEL_ERROR,'Attempt to save lazy object: '.print_r($this->toArray('',true),1));returnfalse;}$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()){returnfalse;}}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__);returnfalse;}$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($pknas$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=newxPDOCriteria($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() Посмотрим на этот метод.
publicfunctionencode($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.
И не забывайте, что это ООП, то есть этот метод можно переопределить.
То есть если тип — 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 сложно и вообще не заслуживает внимания, тот — не я.