-
В приложении MVC, таком как Invision Community, задача контроллера заключается в обработке запросов от пользователей. Он, по своей сути, является посредником, запрашивая данные от модели, делая необходимую обработку и передавая их в представление для отображения (шаблоны) или вывода других данных.
Как описано в документе Маршрутизация и URL-адреса, методы в контроллерах напрямую сопоставляются с URL-адресом. Когда вы посещаете URL-адрес, к примеру такой - community.ru/index.php?app=core&module=messenger&controller=messenger, то контроллер /applications/core/modules/front/messenger/messenger.php инициализируется для обработки запроса.
Все контроллеры расположены в директории /modules приложения. Эта директория имеет две поддиректории.
/front - для модулей, которые обрабатывают фронэнд (общедоступные) функциональные возможности.
/admin - для модулей, которые обрабатывают функциональность админцентра.
Админ контроллеры могут быть доступны только при авторизации пользователя в учетную запись с правами администратора.
Анатомия контроллера
Базовый контроллер:
<?php
namespace IPS\core\modules\front\example;
class _example extends \IPS\Dispatcher\Controller
{
public function manage()
{
//...
}
public function otherMethod()
{
echo "Hello world";
//...
}
}
В этом примере наш контроллер называется 'example'. В результате он сохраняется в файле с именем example.php, что позволяет Invision Community находить его. Наше пространство имён (namespace) должно быть IPS\<app>\modules\<location>\<module>, где app это ключ вашего приложения, location - либо admin, либо front, a module это название модуля, которому принадлежит этот контроллер.
Как написано в документе Автозагрузка классов, имя класса контроллера должно быть строчным (совпадающее с именем файла) и с префиксом подчеркивания; это позволяет Invision Community правильно находить и загружать ваш контроллер, когда это необходимо. Как минимум, контроллеры должны расширять \IPS\Dispatcher\Controller. Существуют другие классы, которые вы можете расширить для дополнительной функциональности, о чем будет сказано ниже.
Методы внутри контроллера являются обработчиками запросов и вызывается, когда параметр do в URL-адресе совпадает с названием метода. В нашем примере выше, URL-адрес community.ru/index.php?app=core&module=example&controller=example&do=otherMethod будет выведена надпись "Hello world".
Специальные методы
Метод execute в контроллере запускается для каждого запроса к обработчику в этом контроллере. Это означает, что это хорошее место для выполнения чего-либо, что применяется ко всем обработчикам - включение CSS или JS файлов для модуля, например. Если вы определяете метод execute() в своем контроллере, вы всегда должны вызывать родителя:
public function execute ()
{
// Ваш код...
parent::execute();
}
Метод manage() это обработчик по умолчанию в контроллере. Если в URL-адресе не передаётся какой-либо параметр - будет вызван метод method().
Методы без запроса
Если у вашего контроллера есть методы, которые не являются обработчиками запросов (например, методы утилит), вы должны указывать префикс подчеркивание перед их названием. Эти методы недоступны по URL-адресу, как стандартные методы.
protected function _someHelper ()
{
// Метод, который наши обаработчики запроса могут вызывать
}
Расширенные контроллеры
Хотя большинство контроллеров расширяют класс \IPS\Dispatcher\Controller, существуют другие контроллеры, которые могут быть расширены вместо этого, чтобы обеспечить дополнительную функциональность (и они, в свою очередь, сами расширяют \IPS\Dispatcher\Controller). Вот они:
\IPS\Node\Controller - Управляет функциональностью для нод - древовидные структуры данных, широко используемые в Invision Community.
\IPS\Content\Controller - Дополнительная функциональность для элементов контента Invision Community.
\IPS\Api\Controller - Обрабатывает запросы API (этот контроллер не расширяет \IPS\Dispatcher\Controller).
Эти контроллеры будут рассмотрены более подробно в следующих документах.
Защита от CSRF-атак
CSRF (Межсайтовая подделка запроса) - это тип веб-атаки, когда пользователь совершает действия, которые он не намеревался выполнять. Чтобы защитить от этого типа атаки, для каждого пользователя создается уникальный ключ, который должен быть включён с действиями, которые делают изменения состояния, а затем проверяется до того, как эти изменения будут сохранены. Invision Community облегчает защиту.
Любой URL-адрес, который выполняет изменение состояния (например, обновляет значение в базе данных или удаляет строку в базе данных), должен выполнить проверку CSRF перед тем, как внести это изменение. Первым шагом является включение ключа CSRF пользователя в URL-адрес обработчика контроллера.При использовании класса \IPS\Http\Url для создания URL-адреса, вы можете сделать это легко, вызвав ->csrf(). Если вы создаете URL-адрес с помощью помощника шаблонов, вы можете просто передать csrf="true" в теге. Пример:
$url = \IPS\Http\Url::internal( "app=application&module=foo&controller=bar" )->csrf();
{url="app=application&module=foo&controller=bar" csrf="true"}
Это добавить параметр &csrfKey=(unique key) к URL-адресу, который вы можете легко проверить в своем методе контроллера следующим образом:
\IPS\Session::i()->csrfCheck();
Этот метод автоматически покажет ошибку, если ключ CSRF недействителен. Это все, что вам нужно сделать! Имейте в виду, что эта проверка должна выполняться для любого изменения состояния (в любое время, когда база данных изменяется, путём перехода по URL-адресу).
-
Что такое активная запись?
Invision Community широко реализует паттерн Active Record, где каждый активный объект записи представляет собой строку в вашей базе данных, и объект предоставляет методы для взаимодействия с этой строкой (включая добавление новых строк). Далее по тексту будем называть активная запись. Например, поскольку модель \IPS\Member возвращает активную запись, мы можем получать и взаимодействовать с строкой пользователем в базе данных следующим образом:
$member = \IPS\Member::load(1);
$member->name = "Валера";
$member->save();
Основной класс активной записи в Invision Community - \IPS\Patterns\ActiveRecord. Большинство классов моделей, с которыми вы будете работать (включая \IPS\Node\Model, \IPS\Content\Item, \IPS\Content\Comment и \IPS\Content\Review), паттерн активной записи, что означает, что все они предлагают совместимый интерфейс для работы с вашими данными.
Важно, чтобы вы использовали методы, предоставленные активной записью, а не напрямую обращались к базе данных, так как при добавлении дополнительных функций к вашему элементу контента, методы будут выполнять более сложные задачи.
Настройка классов ActiveRecord
При написании класса, который расширяет \IPS\Patterns\ActiveRecord в цепочке наследования (даже если вы не расширяете непосредственно его), вам нужно указать некоторые свойства, которые позволяют классу находить записи в базе данных. Вот они:
public static $databaseTable = 'string';
Обязательное. Задает таблицу базы данных, к которой относится данный ActiveRecord.
public static $databasePrefix = 'string';
Необязательное. Указывает префикс поля, используемый этой таблицей. Например, если ваша таблица имеет поля item_id, item_name, item_description и так далее, то свойство $databasePrefix может быть установлено как item_. Это позволяет автоматически сопоставлять свойства со столбцами базы данных.
public static $databaseColumnId = 'string';
Необязательное (по умолчанию: 'id'). Задает поле первичного ключа этой таблицы базы данных.
Загрузка записей
static \IPS\Patterns\ActiveRecord load( int $id [, string $idField=NULL [, mixed $extraWhereClause=NULL ]] )
Пример:
$row = \IPS\YourClass::load(1);
Этот метод извлекает строку с идентификатором 1 из базы данных и возвращает её как экземпляр активной записи вашего класса.
$id (Integer; обязательный) - Идентификатор первичного ключа для загрузки.
$idField (строка; необязательный) - столбец базы данных, относящийся к параметру $id (NULL будет использовать static::$databaseColumnId).
$extraWhereClause (Mixed; optional) - Дополнительный оператор where (смотрите \IPS\Db::build).
Выбрасывается исключение InvalidArgumentException, если $idField не существует в таблице, или исключение OutOfRangeException, если запись с данным идентификатором не найдена.
Примечание. Этот метод не проверяет права пользователя на загружаемый элемент. При написании класса, который расширяет одну из моделей контента (\IPS\Node\Model, \IPS\Content\Item, \IPS\Content\Comment или \IPS\Content\Review, к примеру), вы должны вместо этого использовать loadAndCheckPerms, когда загружаете данные для фронтэнда. Этот метод с такой же конструкцией, что и методы load выше, но loadAndCheckPerms выбросит исключение OutOfRangeException, если у пользователя нет разрешения на просмотр записи.
static \IPS\Patterns\ActiveRecord constructFromData( array $data [, boolean $updateMultitonStoreIfExists=TRUE] )
Пример
$row = \IPS\YourClass::constructFromData( \IPS\Db::i()->select(...)->first() );
Если вы извлекли строку базы данных ручным запросом (например, чтобы получить самую последнюю запись), и вам нужно создать объект ActiveRecord из данных, этот метод позволит вам сделать это без необходимости вызова load (что вызовет другой запрос к базе данных).
$data (Массив; обязательный) - Строка базы данных, вернувшаяся от \IPS\Db как массив.
$updateMultitonStoreIfExists (Boolean; необязательный) - Если true, обновит текущий кэшированный объект, если он существует для данного ID.
Обновление данных в ActiveRecord
Вы можете получить или установить данные для записи просто установив свойства объекта, которые сопоставляются с именами столбцов базы данных. Если вы настроили свойство $databasePrefix в вашем классе, тогда вы не должны включать префикс при ссылке на свойства.
Пример (если в нашей таблице базы данных содержатся столбцы с названием title и description):
$item = \IPS\YourClass::load( 1 );
$item->title = "Заголовок моей статьи";
echo $item->description;
Обратите внимание, что для фактического обновления базы данных метод save следует вызывать после внесения всех изменений (см. ниже).
Если вашему классу необходимо выполнить обработку свойств перед их получением или установкой, вы можете определить геттеры или сеттеры для каждого свойства, добавив метод с именем get_<значение> или set_<значение>. В рамках этих методов вы можете получить доступ к необработанным значениям, используя массив $this->_data.
Пример, переводящий свойство title в верхний регистр:
// YourClass.php
public function set_title( $title )
{
$this->_data['title'] = strtoupper( $title );
}
// OtherClass.php
$item = \IPS\YourClass::load( 1 );
$item->title = 'my title';
echo $item->title;
//--> 'MY TITLE'
Сохранение и удаление записей
void save()
Пример:
$item = \IPS\YourClass::load( 1 );
$item->title = 'New Title';
$item->save();
После изменения данных в ActiveRecord для обновления базы данных необходимо вызвать save.
void delete()
Пример:
$item = \IPS\YourClass::load( 1 );
$item->delete();
Удаляет строку из базы данных.
Клонирование записей
Вы можете просто клонировать записи с помощью выражения clone. \IPS\Patterns\ActiveRecord гарантирует, что первичные ключи настраиваются по мере необходимости. Обратите внимание, что вам все равно нужно вызвать save после клонирования, чтобы создать запись в базе данных.
$item = \IPS\YourClass::load( 1 );
$copy = clone $item;
$copy->save();
echo $copy->id;
//--> 2
Использование побитовых флагов
Класс ActiveRecord реализует поразрядную функцию, позволяющую хранить несколько логических значений в одном поле, без необходимости добавлять в вашу модель новые поля (и, следовательно, столбцы базы данных). Под капотом побитовое поле хранится как целое (integer). Вы определяете свои побитовые флаги как ключи с числовым значением; это числовое значение удваивается для каждого добавляемого вами нового значения (так порядок будет таким 1, 2, 4, 8, 16, 32 и т. д.).
public static $bitOptions = array(
'model_bitoptions' => array(
'model_bitoptions' => array(
'property_1' => 1, // Некоторые опции для этой модели
'property_2' => 2, // Другая опция для этой модели
'property_3' => 4 // Третья опция для модели
)
)
);
В этом примере таблица базы данных нашей модели будет иметь столбец с именем model_bitoptions - и мы используем это имя в массиве $bitOptions для его идентификации. Мы сохраняем три варианта, но вы можете определить больше, следуя шаблону удвоения значения каждый раз. Хорошая практика прокомментировать каждый вариант, объясним, что делает каждый параметр.
Ваша модель ActiveRecord автоматически предоставит объект \IPS\Patterns\Bitwise для этого столбца, который реализует \ArrayAccess, позволяя вам получать и устанавливать значения, поскольку это был массив:
/* Получение значения */
if ( $object->model_bitoptions[‘property_1’] ) {
// ...
}
/* Установка значения - помните, что это может быть только TRUE или FALSE! */
$object->model_bitoptions[‘property_2’] = FALSE;
$object->save();
/* Получение строк базы данных */
$rowsWithPropery1AsTrue = \IPS\Db::i()->select( ‘*’, ‘table’, \IPS \Db::i()->bitwiseWhere( \IPS\YourClass::$bitOptions['model_bitoptions'], 'property_1' ));
-
Доступ к базе данных для записи и выборки данных является необходимостью почти всех приложений и плагинов, которые интегрируются с Invision Community. Класс \IPS\Db обрабатывает соединения с базой данных и расширяет стандартный класс mysqli в основной библиотеке PHP.
Подключение к базе данных
Стандартное соединение с базой данных (обозначенное деталями подключения в файле conf_global.php) может быть установлено автоматически с помощью вызова метода \IPS\Db::i(). Если соединение еще не было установлено, это будет сделано немедленно на лету, когда будет вызван этот метод. Стандартное соединение использует utf8 (или utf8mb4 в зависимости от вашей конфигурации), и все таблицы и столбцы базы данных должны быть настроены на utf8. Это обычно обрабатывается Invision Community, но об этом всё равно важно знать и помнить!
Если вам нужно установить соединение с удаленной базой данных, это можно сделать, передав параметры методу i() класса \IPS\Db. Первый параметр - произвольный идентификатор строкового соединения, а второй параметр - массив с настройками соединения.
$connection = \IPS\Db::i( 'external', array(
'sql_host' => 'localhost',
'sql_user' => 'username',
'sql_pass' => 'password',
'sql_database' => 'database_name',
'sql_port' => 3306,
'sql_socket' => '/var/lib/mysql.sock',
'sql_utf8mb4' => true,
) );
Вам нужно только указать параметры, необходимые для вашего подключения. Вы также можете автоматически поддерживать разделение чтения и записи, передавая те же параметры во второй раз с префиксом «sql_read_» вместо «sql_», указывая на ваш экземпляр MySQL только для чтения.
Выборка данных
Выборка данных из базы данных является одной из наиболее распространенных задач при взаимодействии с базой данных.
/**
* Build SELECT statement
*
* @param array|string $columns The columns (as an array) to select or an expression
* @param array|string $table The table to select from. Either (string) table_name or (array) ( name, alias ) or \IPS\Db\Select object
* @param array|string|NULL $where WHERE clause - see \IPS\Db::compileWhereClause() for details
* @param string|NULL $order ORDER BY clause
* @param array|int $limit Rows to fetch or array( offset, limit )
* @param string|NULL|array $group Column(s) to GROUP BY
* @param array|string|NULL $having HAVING clause (same format as WHERE clause)
* @param int $flags Bitwise flags
* @li \IPS\Db::SELECT_DISTINCT Will use SELECT DISTINCT
* @li \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS Will add SQL_CALC_FOUND_ROWS
* @li \IPS\Db::SELECT_MULTIDIMENSIONAL_JOINS Will return the result as a multidimensional array, with each joined table separately
* @li \IPS\Db::SELECT_FROM_WRITE_SERVER Will send the query to the write server (if read/write separation is enabled)
* @return \IPS\Db\Select
*
*/
public function select( $columns=NULL, $table, $where=NULL, $order=NULL, $limit=NULL, $group=NULL, $having=NULL, $flags=0 )
Вы можете вызвать метод select() для выполнения SELECT запроса к вашей базе данных. Метод возвращает \IPS\Db\Select объект, который позволяет вам в дальнейшем усовершенствовать SELECT запрос. Например, в этом классе есть методы, позволяющие принудительно использовать определенный индекс, делать JOIN запросы к другим таблицам и указывать, какие поля ключ/значение использовать для результатов.
// Получить объект выборки
$select = \IPS\Db::i()->select( '*', 'some_table', array( 'field=?', 1 ), 'some_column DESC', array( 0, 10 ) );
// Использовать указанный индекс для запроса
$select = $select->forceIndex( 'some_index' );
// Присоединить выборку из другой таблицы
$select = $select->join( 'other_table_name', 'other_table_name.column=some_table.column', 'LEFT' );
// Теперь вернем количество полученных результатов
$results = count( $select );
// Сообщить итератору, что ключ должны быть столбцом 'column_a', а значения 'column_b'
$select = $select->setKeyField( 'column_a' )->setValueField( 'column_b' );
// Наконец, перейдем к результатам
foreach( $select as $columnA => $columnB )
{
print $columnA . ': ' . $columnB . '<br>';
}
Здесь есть некоторые важные моменты.
Параметр WHERE принимает много разных форматов, о которых вы должны знать. Вы можете передать строку в качестве оператора WHERE some_column='some value', или вы можете передать массив с первым элементом WHERE, используя ? в качестве плейсхолдеров для значений, а затем каждый плейсхолдер заменяется на последующие записи массива. Это использует подготовленные операторы в MySQL, чтобы избежать проблем с SQL-инъекциями и это рекомендуется array( 'some_column=? OR some_column=?', 'first value', 'second value' ), или, наконец, вы можете передать массив операторов, которые будут объединены вместе AND array( array( 'some_column=?', 'test value' ), array( 'other_column=?', 1 ) ).
Вы можете вызвать setKeyField() без вызова setValueField(). Вместо значения, являющегося строкой в этом случае, это будет просто массив всех выбранных столбцов (или если выбран только один столбец, значение будет строкой с значением этого столбца). Обратите внимание, что вы должны выбрать столбцы, которые вы хотите использовать для setKeyField и/или setValueField.
Определение метода для join() требует, чтобы первый параметр был название таблицы, к которой вы хотите присоединить данные, затем оператор 'on', затем тип присоединения (по умолчанию LEFT). Вы также можете использовать оператор для присоединения данных в качестве четвёртого параметра, если необходимо. Для контролирования по каким столбцам сделать выборку, вы должны изменить первый параметр оператора SELECT (если вы передаёте '*' MySQL вернёт все столбцы из всех таблиц, выбранных из и/или присоединения).
\IPS\Db\Select реализует Iterator и Countable. Это означает, что вы можете рассматривать его как массив и использовать цикл foreach() для обработки результатов, а также вы можете вызвать count() для объекта, чтобы получить количество результатов. Помните, однако, что count() по умолчанию только возвращает количество результатов, возвращаемых запросом. Если у вас 1000 строк в таблице и вы используйте оператор limit, чтобы возвращать только 100 результатов, тогда count() покажет 100. Если вы хотите получить общее количество строк в таблице, как буд-то оператор limit не используется, вы можете передать флаг \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS в стандартный метод select(), как отмечено блоком phpdoc выше.
Часто вы выбираете только одну строку (то есть при выполнении запроса COUNT(*)), встроенный метод first() облегчает это. Однако имейте в виду, что если строка не существует, генерируется исключение UnderflowException, поэтому вы должны перенести такие запросы в оператор try/catch.
try
{
$row = \IPS\Db::i()->select( '*', 'table', array( 'id=?', 2 ) )->first();
}
catch( \UnderflowException $e )
{
// В таблице нет строки с id = 2
}
Вставка, обновление и удаление строк
Вы также захотите периодически вставлять, обновлять и удалять строки в MySQL. Для этого вы можете использовать соответствующие методы insert(), update() и delete().
// Вставить строку
$new_id = \IPS\Db::i()->insert( 'some_table', array( 'column_one' => 'value', 'column_two' => 'value2' ) );
// Обновить эту строку
\IPS\Db::i()->update( 'some_table', array( 'column_two' => 'other value' ), array( 'id_column=?', $new_id ) );
// Удалить строку
\IPS\Db::i()->delete( 'some_table', array( 'id_column=?', $new_id ) );
Первым параметром метода добавления строки является имя таблицы, далее следует ассоциативный массив названий столбцов => значения. Метод возвращает новый автоинкрементный (если применимо) ID.
Обратите внимание, что существует также метод replace(), который ведет себя как insert(), только будет выполнено запрос REPLACE INTO вместо INSERT INTO (в этом случае, если встречается повторяющийся уникальный индекс, оригинал будет заменен новой строкой).
Метод update() ожидает, что первым параметром будет имя таблицы, второй параметр будет ассоциативным массивом имен столбцов => значений, а третьим параметром будет оператор WHERE (если необходимо). Кроме того, при необходимости вы можете передать массив присоединений таблиц (оператор JOIN) в качестве четвёртого параметра, если он необходим, массив, представляющий оператор limit как пятый параметр и флаги для изменения запроса в качестве последнего параметра, в том числе:
\IPS\Db::LOW_PRIORITY Использует LOW_PRIORITY
\IPS\Db::IGNORE Использует IGNORE
Метод delete() обычно вызывается только с первым параметром, именем таблицы, для удаления всей таблицы, а также со вторым параметром оператором where для удаления определенных строк. Метод дополнительно принимает третий параметр для управления упорядочивания результатов для запроса DELETE, четвёртый параметр для ограничения количества результатов удаления, и пятый столбце, который указывает столбец оператора, если оператор WHERE является оператором.
О структуре базы данных
Вы можете создавать таблицы базы данных, добавлять, изменять и удалять столбцы из существующих таблиц, изменять и удалять индекс из существующих таблиц с помощью библиотеки \IPS\Db. Кроме того, существуют методы, позволяющие определить, существует ли таблица, столбец или индекс, прежде чем использовать его.
/**
* Существует ли таблица?
*
* @param string $name Table Name
* @return bool
*/
public function checkForTable( $name )
/**
* Существует ли столбец?
*
* @param string $name Table Name
* @param string $column Column Name
* @return bool
*/
public function checkForColumn( $name, $column )
/**
* Существует ли индекс?
*
* @param string $name Table Name
* @param string $index Index Name
* @return bool
*/
public function checkForIndex( $name, $index )
/**
* Создать таблицу
*
* @code
\IPS\Db::createTable( array(
'name' => 'table_name', // Table name
'columns' => array( ... ), // Column data - see \IPS\Db::compileColumnDefinition for details
'indexes' => array( ... ), // (Optional) Index data - see \IPS\Db::compileIndexDefinition for details
'comment' => '...', // (Optional) Table comment
'engine' => 'MEMORY', // (Optional) Engine to use - will default to not specifying one, unless a FULLTEXT index is specified, in which case MyISAM is forced
'temporary' => TRUE, // (Optional) Will sepcify CREATE TEMPORARY TABLE - defaults to FALSE
'if_not_exists' => TRUE, // (Optional) Will sepcify CREATE TABLE name IF NOT EXISTS - defaults to FALSE
) );
* @endcode
* @param array $data Table Definition (see code sample for details)
* @throws \IPS\Db\Exception
* @return void|string
*/
public function createTable( $data )
/**
* Создать копию структуры таблицы
*
* @param string $table The table name
* @param string $newTableName Name of table to create
* @throws \IPS\Db\Exception
* @return void|string
*/
public function duplicateTableStructure( $table, $newTableName )
/**
* Переименовать таблицу
*
* @see <a href='http://dev.mysql.com/doc/refman/5.1/en/rename-table.html'>Rename Table</a>
* @param string $oldName The current table name
* @param string $newName The new name
* @return void
* @see <a href='http://stackoverflow.com/questions/12856783/best-practice-with-mysql-innodb-to-rename-huge-table-when-table-with-same-name-a'>Renaming huge InnoDB tables</a>
* @see <a href='http://www.percona.com/blog/2011/02/03/performance-problem-with-innodb-and-drop-table/'>Performance problem dropping huge InnoDB tables</a>
* @note A race condition can occur sometimes with InnoDB + innodb_file_per_table so we can't drop then rename...see above links
*/
public function renameTable( $oldName, $newName )
/**
* Изменить таблицу
* Можно обновлять комментарии и движок
* @note This will not examine key lengths and adjust.
*
* @param string $table Table name
* @param string|null $comment Table comment. NULL to not change
* @param string|null $engine Engine to use. NULL to not change
* @return void
*/
public function alterTable( $table, $comment=NULL, $engine=NULL )
/**
* Удалить таблицу
*
* @see <a href='http://dev.mysql.com/doc/refman/5.1/en/drop-table.html'>DROP TABLE Syntax</a>
* @param string|array $table Table Name(s)
* @param bool $ifExists Adds an "IF EXISTS" clause to the query
* @param bool $temporary Table is temporary?
* @return mixed
*/
public function dropTable( $table, $ifExists=FALSE, $temporary=FALSE )
/**
* Добавить столбец в таблицу в базе данных
*
* @see \IPS\Db::compileColumnDefinition
* @param string $table Table name
* @param array $definition Column Definition (see \IPS\Db::compileColumnDefinition for details)
* @return void
*/
public function addColumn( $table, $definition )
/**
* Изменить существующий столбец
*
* @see \IPS\Db::compileColumnDefinition
* @param string $table Table name
* @param string $column Column name
* @param array $definition New column definition (see \IPS\Db::compileColumnDefinition for details)
* @return void
*/
public function changeColumn( $table, $column, $definition )
/**
* Удалить столбец
*
* @param string $table Table name
* @param string|array $column Column name
* @return void
*/
public function dropColumn( $table, $column )
/**
* Добавить индекс в таблицу в базе данных
*
* @see \IPS\Db::compileIndexDefinition
* @param string $table Table name
* @param array $definition Index Definition (see \IPS\Db::compileIndexDefinition for details)
* @param bool $discardDuplicates If adding a unique index, should duplicates be discarded? (If FALSE and there are any, an exception will be thrown)
* @return void
*/
public function addIndex( $table, $definition, $discardDuplicates=TRUE )
/**
* Изменить существующий индекс
*
* @see \IPS\Db::compileIndexDefinition
* @param string $table Table name
* @param string $index Index name
* @param array $definition New index definition (see \IPS\Db::compileIndexDefinition for details)
* @return void
*/
public function changeIndex( $table, $index, $definition )
/**
* Удалить индекс
*
* @param string $table Table name
* @param string|array $index Column name
* @return mixed
*/
public function dropIndex( $table, $index )
Большинство из этих методов описаны в phpdoc и редко используются, за исключением случаев использования центра разработчика для добавления запросов на обновлений.
Разное
Наконец, в классе есть несколько методов и свойств, которые могут оказаться полезными или актуальными при работе с драйвером базы данных. Если вам нужно получить префикс базы данных, используемый у таблиц (он же sql_tbl_prefix в conf_global.php), вы можете сделать это, вызвав \IPS\Db::i()->prefix . Если вы создаете запросы для запуска вручную, вам нужно будет добавить его к именам ваших таблиц.
Если вам нужно построить SQL оператор и вернуть её вместо запуска, вы можете установить \IPS\Db::i()->returnQuery = TRUE перед вызовом драйвера для построения запроса.
Для запуска MySQL запроса, который был полностью построен и представляет из себя строку, вы можете можете вызвать метод query(). Например:
\IPS\Db::i()->query( "UPDATE some_table SET field_a='value' WHERE id_field=1" );
Обычно вам следует избегать непосредственного использования метода query(), поскольку другие встроенные методы автоматически обрабатывают такие вещи, как экранирование значений, добавление префикса таблицы базы данных и т.д.
Если вам нужно создать инструкцию UNION, для облегчения этого метода также существует способ.
/**
* Построение инструкции UNION
*
* @param array $selects Array of \IPS\Db\Select objects
* @param string|NULL $order ORDER BY clause
* @param array|int $limit Rows to fetch or array( offset, limit )
* @param string|null $group Group by clause
* @param bool $unionAll TRUE to perform a UNION ALL, FALSE (default) to perform a regular UNION
* @param int $flags Bitwise flags
* @param array|string|NULL $where WHERE clause (see example)
* @param string $querySelect Custom select for the outer query
* @li \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS Will add SQL_CALC_FOUND_ROWS
* @return \IPS\Db|Select
*/
public function union( $selects, $order, $limit, $group=NULL, $unionAll=FALSE, $flags=0, $where=NULL, $querySelect='*' )
Для построения оператора FIND_IN_SET(), который позволяет запросу искать определенные значения в поле MySQL, которое содержит значения, разделенные запятыми, вы можете использовать метод findInSet().
/**
* FIND_IN_SET
* Generates a WHERE clause to determine if any value from a column containing a comma-delimined list matches any value from an array
*
* @param string $column Column name (which contains a comma-delimited list)
* @param array $values Acceptable values
* @param bool $reverse If true, will match cases where NO values from $column match any from $values
* @return string Where clause
* @see \IPS\Db::in() More efficient equivilant for columns that do not contain comma-delimited lists
*/
public function findInSet( $column, $values, $reverse=FALSE )
Аналогичным образом вы можете построить оператор IN() с помощью метода in():
/**
* IN
* Generates a WHERE clause to determine if the value of a column matches any value from an array
*
* @param string $column Column name
* @param array $values Acceptable values
* @param bool $reverse If true, will match cases where $column does NOT match $values
* @return string Where clause
* @see \IPS\Db::findInSet() For columns that contain comma-delimited lists
*/
public function in( $column, $values, $reverse=FALSE )
Если вы выполняете запрос по побитовому столбцу и вам нужно проверить значение, вы можете использовать метод bitwiseWhere (или просто построить оператор WHERE вручную).
/**
* Bitwise WHERE clause
*
* @param array $definition Bitwise keys as defined by the class
* @param string $key The key to check for
* @param bool $value Value to check for
* @return string
* @throws \InvalidArgumentException
*/
public function bitwiseWhere( $definition, $key, $value=TRUE )
Вы обнаружите, что большинство этих разных методов не так часто используются, как методы ядра insert, update, delete, replace и select.
-
В приложении MVC модель отвечает за взаимодействие с данными и передачу их контроллеру. То же самое и в Invision Community.
Вообще говоря, экземпляр модели относится к предмету. Например, если бы у вас были форумы и темы, Форум был бы модель, как и Тема. Модель для каждого из них предоставляет методы, позволяющие взаимодействовать с ними. При разработке моделей для вашего приложения перечислите, с чем работает ваше приложение, - это, скорее всего, и будут ваши модели.
Название и местоположение
Название модели всегда должно быть существительным в единственном числе; то есть модель всегда относится к одной вещи. Названия моделей так же должны быть в формате PascalCase.
Модели расположены в директории /sources вашего приложения. Как обсуждалось в документе Автозагрузка классов, в этой директории должны быть подпапку с таким же названием, как и ваша модель, а сама модель должна находится в данной поддиректории (вместе с любыми поддерживающими модулями).
/sources/topic/topic.php - некорректно: директория/модель в нижнем регистре.
/sources/Topics/Topics.php - некорректно: множественное имя модели
/sources/Topic/Topic.php - Корректно
Еще раз обращаясь к руководству по автозагрузке, имя класса в файле PHP должно иметь префикс с подчеркиванием. Например, если ваш файл был назван Topic.php, класс модели, который он содержит, должен называться _Topic.
Базовые модели
Существует несколько базовых моделей, которые часто будут расширять ваши модели. Каждый из них будет рассмотрен более подробно - включая полный обзор методов, которые они предоставляют - в соответствующих разделах в этой документации. Базовыми являются:
\IPS\Node\Model - Методы для работы с нодами, древовидная структура, применимая для категорий (и многое другое).
\IPS\Content\Item - Методы для работы с элементами контента, например, темы, изображения галереи.
\IPS\Content\Comment - Методы для работы с комментариями (включая сообщения форумов).
\IPS\Content\Review - Методы для работы с отзывами.
Эти четыре базовые модели реализуют паттерн Active Record в Invision Community.
Пример: /exampleApp/sources/Counter/Counter.php
Очень простая автономная модель, которая предоставляет метод для хранения и увеличения числа.
<?php
namespace IPS\exampleApp;
class _Counter
{
protected $counter = 0;
public function incrementCounter ()
{
$this->counter++;
}
}
/exampleApp/modules/front/example/index.php
Контроллер, который создает экземпляр вышеуказанной модели, затем вызывает метод incrementCounter, который он предоставляет.
namespace IPS\exampleApp\modules\front\example;
class _index extends \IPS\Dispatcher\Controller
{
public function manage()
{
$myCounter = new \IPS\exampleApp\Counter();
$myCounter->incrementCounter();
}
}
-
Фреймворк Invision Community предоставляет мощный вспомогательный класс для работы с URL-адресами, включая парсинг адресов и создание запросов (так же чтение ответов) URL-адресов. Программное обеспечение автоматически использует cURL, если доступно, и обратно возвращается к стандартным сокетам (это поведение также может быть переопределено константами, если среда хочет принудительно использовать cURL или сокеты).
Работа с URL-адресами
Класс \IPS\Http\Url используется для работы с URL-адресами, а два вспомогательных метода образуют основной интерфейс для создания объекта URL с этим классом:
internal(): Используйте этот метод при создании внутреннего URL-адреса, например URL-адрес форума или темы. Этот метод автоматически создаёт дружественные URL-адреса, если необходимо.
external(): Передайте полный URL-адрес этому методу для создания объекта URL-адреса из строки URL-адреса.
// Создание URL-адреса на тему с ID = 1
$url = \IPS\Http\Url::internal( 'app=forums&module=forums&controller=topic&id=1', 'front', 'forums_topic', array( 'topic-friendly-slug' ) );
$url = \IPS\Http\Url::external( 'https://www.google.com' );
// Создание объекта URL для стандартной строки URL
Внутренний метод принимает следующие параметры:
Строка запроса URL-адреса.
'база' ('front' или 'admin', в зависимости от того, является ли URL-адрес на страницу админцентра или нет).
SEO шаблон, если применимо.
Массив SEO слагов, если шаблон их вызывает.
Константу PROTOCOL_* из класса \IPS\Http\Url для переопределения должен ли быть сгенерирован http или https URL-адрес. Вы можете опустить этот параметр и позволить программному обеспечению правильно определить протокол, который будет использоваться автоматически.
Внешний метод просто принимает строку URL-адреса.
Если вы не уверены, что объект URL, который вы создаете, является внутренним или внешним, вы можете альтернативно использовать метод createFromString() (передавая полный URL-адрес так же, как и с помощью метода external()), однако обратите внимание, что этот метод является ресурсоёмким по производительности и предпочтительны непосредственно методы internal() и external(). Первым параметром для этого метода является URL-адрес, второй параметр - логический флаг, указывающий, может ли URL-адрес являться дружественным внутренним URL-адресом, а третий и последний параметр - логическим флагом, указывающим, хотите ли вы автоматически кодировать любые недопустимые компоненты вместо того, чтобы бросать ошибку (по умолчанию FALSE - установите значение TRUE, если URL-адрес предоставлен пользователем и не должен выдавать ошибку).
Вы можете внести корректировки в URL-адрес после создания объекта URL-адреса, вызвав различные методы.
setScheme(): Вы можете передать схему в (то есть http или https), или передать в NULL для использования относительной схемы (то есть без схемы вовсе).
setHost(): Принимает полное имя хоста.
setPath(): Принимает полный допустимый путь.
setQueryString(): Принимает ключ строки запроса в качестве первого параметра и его значение как второй параметр ИЛИ массив пар ключ => значение в качестве первого параметра.
setFragment(): Принимает фрагмент.
stripQueryString(): Принимает ключ строки запроса или массив ключей скроки запроса и удаляет эти параметры строки запроса (если есть) из URL-адреса.
Когда вы хотите вывести URL-адрес, вы можете использовать объект \IPS\Http\Url как строку:
print (string) $urlObject;
Класс имеет несколько дополнительных свойств и методов, на которые вы, возможно, захотите ссылаться или вызывать, описанные ниже:
isInternal: свойство указывает, является ли URL-адрес внутренним или нет.
isFriendly: свойство указывает, является ли URL-адрес дружественным (внутренним) URL-адресом или нет.
queryString: свойство является массивом ключ => значение параметров строки запроса в URL-адресе.
hiddenQueryString: свойство является массивом ключ => значение параметров строки запроса, которые были бы, если URL-адрес не был дружественным URL-адресом. Например, если вы создаете внутренний дружественный объект URL, это свойство будет содержать связанные параметры строки запроса, которые не отображаются (поскольку используется дружественный URL-адрес).
csrf(): Вызовите этот метод, чтобы добавить ключ CSRF текущего пользователя в URL-адрес как аргумент строки запроса. Затем это будет проверено контроллерами для предотвращения атак типа CSRF. Если у вас есть запрос на изменение состояния, который не использует класс \IPS\Helpers\Form, обычно должны проверять ключ CSRF.
(static) seoTitle(): Вы можете вызвать этот метод, чтобы создать допустимый дружественный URL слаг. Обычно это используется, когда контент уже добавлен и URL слаг сохранён для следующего обращения, однако вы можете также вызывать этот метод «на лету», если это необходимо.
Создание запросов
Создав объект класса \IPS\Http\Url, вы можете делать запросы к нему. Для этого вызывается метод request().
/**
* Make a HTTP Request
*
* @param int|null $timeout Timeout
* @param string $httpVersion HTTP Version
* @param bool|int $followRedirects Automatically follow redirects? If a number is provided, will follow up to that number of redirects
* @return \IPS\Http\Request
*/
public function request( $timeout=null, $httpVersion=null, $followRedirects=5 )
Он возвращает объект \IPS\Http\Request. Ниже приведены некоторые методы, которые вы можете вызвать перед выполнением запроса:
login(): Принимает имя пользователя в качестве первого параметра и пароль в качестве второго параметра и выполняет основной запрос авторизации по URL.
setHeaders(): Принимает массив пар ключ => значение заголовков, которые должны быть включены в запрос.
sslCheck(): Принимает логическое значение true или false как единственный параметр, сигнализирующий, должны ли проверяться SSL сертификаты или нет. В большинстве случаев это должно быть оставлено по умолчанию (true), если только вы не знаете, что SSL URL-адрес, к которому вы делаете запрос, имеет недопустимый сертификат.
forceTls(): Обязать TLS для запроса. Это прежде всего используется с некоторыми платежными шлюзами, которые обеспечивают выполнение запросов TLS.
Впоследствии вы можете сделать запрос. Для этого вы вызываете метод запроса, который хотите выполнить (например для выполнения GET запроса, нужно вызвать метод get(), для PUT запроса соответственно put()), передавая любые параметры, которые должны быть включены в запрос (для запросов POST и PUT).
$request = \IPS\Http\Url::external( "http://someurl.com" )->request()->get();
Это возвращает объект \IPS\Http\Response, который вы можете теперь проверять и манипулировать по мере необходимости. Во-первых, есть несколько полезных свойств, которые вам могут потребоваться:
httpResponseVersion: Версия протокола HTTP запроса (1.0 или 1.1, обычно).
httpResponseCode: Код ответа HTTP. Возможно, вам потребуется проверить, что после запроса был возвращен действительный код ответа (т.е. 200).
httpResponseText: Это текст ответа HTTP. Например, для запроса 200 это будет "ОК".
httpHeaders: Массив, содержащий все HTTP заголовки ответа в виде пар ключ => значение.
cookies: Массив всех заголовков Set-Cookie в виде пар ключ => значение.
content: Тело ответа.
Отправка ответа в виде строки возвращает свойство содержимого, указанное выше.
В классе \IPS\Http\Response есть несколько методов, которые можно использовать, чтобы упростить работу с некоторыми распространенными ответами.
decodeJson(): Вызов этого метода запустит ответ через json_decode перед его возвратом. Если ответ не в формате JSON, будет выброшено исключение RuntimeException.
decodeXml(): Вызов этого метода спарсит ответ как XML, если ответ не в формате XML, будет выброшено исключение RuntimeException.
Кроме того, стоит отметить, что если запрос по какой-либо причине даёт сбой (например, тайм-аут подключения к удалённому серверу), выбрасывается исключение \IPS\Http\Request\Exception. С этой целью вы должны оборачивать запросы в блоки try/catch.
// Create a URL object
$url = \IPS\Http\Url::external( "http://someurl.com" )->setQueryString( 'key', 'value' );
// Построение и декодирование с помощью JSON
try
{
$response = $url->request()->get()->decodeJson();
}
catch( \IPS\Http\Request\Exception $e )
{
die( "There was a problem fetching the request" );
}
catch( \RuntimeException $e )
{
die( "The response was not valid JSON" );
}
var_dump( $response );
exit;
-
Invision Community имеет класс для работы с данными запроса, включая данные GET, POST, и REQUEST, куками и обнаружение определенной информации о запросе (например, были ли они отправлены через AJAX или нет). Вам нужно будет использовать этот класс для выполнения некоторых общих действий, работающих с программным обеспечением. Класс доступен с помощью \IPS\Requst::i() и реализует паттерн Одиночка (Singleton).
Данные GET, POST и REQUEST
Чтобы получить доступ к переменным запроса, вы просто вызываете их как свойства класса. Например, вы можете проверить, установлена ли переменная запроса и вывести её таким образом:
if( isset( \IPS\Request::i()->someVariable ) )
{
print \IPS\Request::i()->someVariable;
}
Данные запроса в основном не изменены из первоначально представленных данных, за исключением того, что нулевые байты и символы управления RTL удаляются из ввода, и удалены слэши, если включены магические кавычки. Это означает, что все данные запроса считаются потенциально зараженными, и вам нужно будет принять меры предосторожности, чтобы не создать проблемы в безопасности, полагаясь на 'чистые' данные запроса этого класса.
Если запрос выполняется с использованием метода PUT (например в REST API в некоторых случаях), эти данные запроса также доступны через этот класс.
Работа с cookie
Значения Cookie доступны в свойстве cookie класса \IPS\Request.
print \IPS\Request::i()->cookie['member_id'];
Если ваш сайт использует префикс cookie, обратите внимание, что здесь он будет автоматически удалён.
Чтобы установить cookie, вы можете использовать метод setCookie. Как и при извлечении cookie, вы не должны включать префикс cookie, который используется.
/**
* Set a cookie
*
* @param string $name Name
* @param mixed $value Value
* @param \IPS\DateTime|null $expire Expiration date, or NULL for on session end
* @param bool $httpOnly When TRUE the cookie will be made accessible only through the HTTP protocol
* @param string|null $domain Domain to set to. If NULL, will be detected automatically.
* @param string|null $path Path to set to. If NULL, will be detected automatically.
* @return bool
*/
public function setCookie( $name, $value, $expire=NULL, $httpOnly=TRUE, $domain=NULL, $path=NULL )
Обычно вы должны оставить $domain и $path как NULL, однако при необходимости они могут быть переопределены, например, если вы работаете над интеграцией с сторонним сервисом.
Вы можете очистить все cookies авторизации с помощью метода \IPS\Request::i()->clearLoginCookies(), если необходимо, включая member_id и pass_hash, а также любые cookies пароля (где пользователь может иметь введённый пароль для доступа к форуму).
Другие вспомогательные методы
В классе \IPS\Request есть несколько вспомогательных методов, которые можно использовать при необходимости для проверки различных свойств запроса:
\IPS\Request::i()->isAjax() - Возвращает логическое значение true или false, указывающее, был ли запрос выполнен через AJAX.
\IPS\Request::i()->isSecure() - Возвращает логическое значение true или false, указывающее, был ли запрос выполнен через SSL (https).
\IPS\Request::i()->url() - Возвращает объект \IPS\Http\Url, представляющий запрошенный URL-адрес. Обратите внимание, что фрагменты (значения после символа хеша в URL-адресе) не отправляются на сервер и не будут доступны для проверки на уровне сервера.
\IPS\Request::i()->ipAddress() - Возвращает IP-адрес, используемый при выполнении текущего запроса, с учетом с учетом прокси-серверов и перенаправления, если администратор решил сделать это в админцентре.
\IPS\Request::i()->ipAddressIsBanned() - Возвращает логическое значение true или false, указывающее, забанен ли текущий IP-адрес запроса в бан-фильтре IP-адресов в админцентре.
\IPS\Request::i()->requestMethod() - Возвращает текущий метод запроса, в верхнем регистре.
\IPS\Request::i()->isCgi() - Возвращает логическое значение true или false, указывающее, обрабатывается ли текущий запрос CGI-оболочкой в PHP.
\IPS\Request::i()->floodCheck() - Проверяет настройку поискового флуда пользователя , чтобы определить, искал ли что-то пользователь в последнее время и что пользователь снова ищет через короткий промежуток времени. Если пользователь недавно выполнил поиск и этот метод вызывается до того, как завершился период времени контроля флуда, пользователю будет показана ошибка, в противном случае его время последнего поиска обновляется до текущего времени для последующих проверок.
\IPS\Request::i()->confirmedDelete( $title, $message, $submit ) - Когда пользователь удаляет данные, разумно утверждать, что действие было предпринято осознано, а не случайно. Чтобы облегчить это, вы можете вызвать метод confirmedDelete(), который проверит подтвердил ли пользователь удаление контента, и если нет, покажет сначала скрин подтверждения.
-
Обработка даты и времени является важной функцией программного обеспечения, а класс \IPS\DateTime предоставляет несколько методов, которые помогают надежно обрабатывать даты и время. Важно отметить, что класс \IPS\DateTime расширяет встроенный PHP DateTime класс, поэтому все общие методы PHP для работы с датами и временем также доступны через этот интерфейс.
Дата и время представлены в базе данных в виде UNIX-времени (timestamp). Однако, когда вы показываете дату пользователю, нам нужно преобразовать UNIX-время в удобную для чтения дату и время, локализованные в часовом поясе пользователя. Для этой цели можно использовать статический метод ts():
$time = \IPS\DateTime::ts( $timestamp );
Вы также можете использовать статический метод create() для создания нового экземпляра datetime (который будет по умолчанию использовать текущую дату/время).
$time = \IPS\DateTime::create();
Основные методы, которые вы будете использовать для отображения даты и/или времени, следующие:
/* Показывает время и дату в часовом поясе пользователя */
print (string) $time;
Магический метод __toString() автоматически позаботится о преобразованиях часового пояса и т.д.
/* Вывод HTML тега <time> HTML с отображением относительного времени */
print $time->html( TRUE, FALSE, NULL );
Первый параметр определяет, должна ли дата/время быть в формате capitalize (первый символ каждого слова будет заглавным; остальные символы свой вид не меняют) или нет (установите его в значение FALSE, если время будет использоваться в середине предложения, например), а второй параметр определяет, будет ли использоваться 'короткая' версия дата/время, даже не на мобильном устройстве (например 1д вместо 1 день). Последний параметр позволяет переопределить пользователя или язык для форматирования времени.
/* Показывать только дату */
print $time->localeDate();
/* Показывать только время - первый параметр указывает, следует ли возвращать секунды, а второй параметр указывает, следует ли возвращать минуты */
print $time->localeTime( TRUE, TRUE );
/* Возвращает только месяц и день, без указания года (или времени)
print $time->dayAndMonth();
/* Возвращает дату с 4-х значным годом */
print $time->fullYearLocaleDate();
/* Форматирует относительную дату/время */
print $time->relative( \IPS\DateTime::RELATIVE_FORMAT_NORMAL );
Для метода relative() распознаются следующие константы:
RELATIVE_FORMAT_NORMAL: Вчера в 2 часа.
RELATIVE_FORMAT_LOWER: вчера в 2 часа (например "Изменено вчера в 2 часа").
RELATIVE_FORMAT_SHORT: 1д (для мобильных устройств).
Если вам нужно использовать полностью настраиваемый формат, вы можете использовать метод strFormat(). Хотя класс DateTime в PHP уже имеет встроенный метод format(), но он не понимает локали, поэтому используется метод strFormat() (который принимает любой формат, принятый strftime в PHP).
print $time->strFormat( '%B' );
Наконец, есть некоторые стандартизированные форматы, которые поддерживаются что называется "из коробки", в первую очередь полезны, когда спецификации требуют, чтобы даты были отформатированы определенным образом (например, RSS):
print $time->rfc3339(); // 2017-06-06T11:00:00Z
print $time->rfc1123(); // Вторник, 6 Июня 2017 11:00:00 GMT
Важно помнить при кешировании данных, что даты и время должны быть локализованы на основе текущего часового пояса пользователя (который определяется автоматически). По этой причине вы не должны кэшировать отформатированные даты и время, но можете форматировать при отображении.
Наконец, существует плагин шаблона "datetime", который можно использовать для автоматического форматирования дат и времени в шаблонах.
{datetime="$timestamp"}
Это отобразит локализованную дату и время из вызова __toString(). Дополнительные атрибуты, поддерживаемые этим плагином:
dateonly: Вернуть только дату.
norelative: Не возвращать относительную дату.
lowercase: Вернуть дату в нижнем регистре.
short: Вернуть короткую форму даты.