Fi1osof 22 марта 2015 6 7
Пишу топик с описанием новейших технологий, корни которых берут свое начало еще вот в этом топике, написанном более двух лет назад. Решил его перенести сюда. Почитайте пока, а я статью свою допишу. Она довольно интересная :)

Один из самых главных барьеров в переходе с 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',

Как это работает? (вкратце)
  1. Мы создали классы page и page_mysql, расширяющие объект xPDOObject (так как именно объекты xPDOObject имеют необходимый функционал для взаимодействия с базой данных, типа load(),save(), remove() и т.п.)
  2. Мы добавили описание своего объекта в окружение xPDO ($modx->map['page']) (не забывает, что MODx — это расширение класса xPDO).
  3. Получили объект (выполнили запрос)
    $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 сложно и вообще не заслуживает внимания, тот — не я.
7 комментариев
n
niibaca-nah 24 марта 2015г в 00:24 #
БлагоДарю тебя друг за такой интереснейший пост!!!

Ближе к середине был уверен, что выражу благодарность в комментариях обязательно. :)
Fi1osof1
Fi1osof 24 марта 2015г в 12:51 #
Всегда пожалуйста! :)
z
zesen 05 июня 2015г в 16:37 #
Хорошая статья, спасибо за предоставленный материал
Fi1osof1
Fi1osof 08 июня 2015г в 20:11 #
Пожалуйста.
y
young_man 02 декабря 2015г в 17:45 #
Спасибо большое, очень полезная статья)
y
young_man 02 декабря 2015г в 17:48 #
А как выбрать из таблицы в БД массив строк?
Например, я пишу в сниппете:

<?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');


Возвращается результат только первой записи в таблице, ассоциированной с данным пользователем
y
young_man 02 декабря 2015г в 18:00 #
Всё, решил вопрос через getCollection)
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.