Browse Source

refactored logging.

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
fb75c95813
  1. 18
      framework/YiiBase.php
  2. 6
      framework/base/Application.php
  3. 48
      framework/logging/DbTarget.php
  4. 47
      framework/logging/FileTarget.php
  5. 131
      framework/logging/Logger.php
  6. 26
      framework/logging/Router.php
  7. 93
      framework/logging/Target.php
  8. 23
      todo.md

18
framework/YiiBase.php

@ -8,7 +8,9 @@
*/
use yii\base\Exception;
use yii\logging\Logger;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException;
/**
* Gets the application start timestamp.
@ -394,7 +396,7 @@ class YiiBase
public static function trace($message, $category = 'application')
{
if (YII_DEBUG) {
self::getLogger()->trace($message, $category);
self::getLogger()->log($message, Logger::LEVEL_TRACE, $category);
}
}
@ -407,7 +409,7 @@ class YiiBase
*/
public static function error($message, $category = 'application')
{
self::getLogger()->error($message, $category);
self::getLogger()->log($message, Logger::LEVEL_ERROR, $category);
}
/**
@ -419,7 +421,7 @@ class YiiBase
*/
public static function warning($message, $category = 'application')
{
self::getLogger()->warning($message, $category);
self::getLogger()->log($message, Logger::LEVEL_WARNING, $category);
}
/**
@ -431,7 +433,7 @@ class YiiBase
*/
public static function info($message, $category = 'application')
{
self::getLogger()->info($message, $category);
self::getLogger()->log($message, Logger::LEVEL_INFO, $category);
}
/**
@ -453,7 +455,7 @@ class YiiBase
*/
public static function beginProfile($token, $category = 'application')
{
self::getLogger()->beginProfile($token, $category);
self::getLogger()->log($token, Logger::LEVEL_PROFILE_BEGIN, $category);
}
/**
@ -465,7 +467,7 @@ class YiiBase
*/
public static function endProfile($token, $category = 'application')
{
self::getLogger()->endProfile($token, $category);
self::getLogger()->log($token, Logger::LEVEL_PROFILE_END, $category);
}
/**
@ -477,13 +479,13 @@ class YiiBase
if (self::$_logger !== null) {
return self::$_logger;
} else {
return self::$_logger = new \yii\logging\Logger;
return self::$_logger = new Logger;
}
}
/**
* Sets the logger object.
* @param \yii\logging\Logger $logger the logger object.
* @param Logger $logger the logger object.
*/
public static function setLogger($logger)
{

6
framework/base/Application.php

@ -75,6 +75,8 @@ use yii\base\InvalidCallException;
*/
class Application extends Module
{
const EVENT_BEFORE_REQUEST = 'beforeRequest';
const EVENT_AFTER_REQUEST = 'afterRequest';
/**
* @var string the application name. Defaults to 'My Application'.
*/
@ -178,7 +180,7 @@ class Application extends Module
*/
public function beforeRequest()
{
$this->trigger('beforeRequest');
$this->trigger(self::EVENT_BEFORE_REQUEST);
}
/**
@ -186,7 +188,7 @@ class Application extends Module
*/
public function afterRequest()
{
$this->trigger('afterRequest');
$this->trigger(self::EVENT_AFTER_REQUEST);
}
/**

48
framework/logging/DbTarget.php

@ -9,6 +9,9 @@
namespace yii\logging;
use yii\db\Connection;
use yii\base\InvalidConfigException;
/**
* DbTarget stores log messages in a database table.
*
@ -23,23 +26,21 @@ namespace yii\logging;
class DbTarget extends Target
{
/**
* @var string the ID of [[\yii\db\Connection]] application component.
* @var string the ID of [[Connection]] application component.
* Defaults to 'db'. Please make sure that your database contains a table
* whose name is as specified in [[tableName]] and has the required table structure.
* @see tableName
*/
public $connectionID = 'db';
/**
* @var string the name of the DB table that stores log messages. Defaults to '{{log}}'.
* If you are using table prefix 'tbl_' (configured via [[\yii\db\Connection::tablePrefix]]),
* it means the DB table would be named as 'tbl_log'.
* @var string the name of the DB table that stores log messages. Defaults to 'tbl_log'.
*
* The DB table must have the following structure:
* The DB table should have the following structure:
*
* ~~~
* CREATE TABLE tbl_log (
* id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
* level VARCHAR(32),
* level INTEGER,
* category VARCHAR(255),
* log_time INTEGER,
* message TEXT,
@ -50,42 +51,53 @@ class DbTarget extends Target
*
* Note that the 'id' column must be created as an auto-incremental column.
* The above SQL shows the syntax of MySQL. If you are using other DBMS, you need
* to adjust it accordingly. For example, in PosgreSQL, it should be `id SERIAL PRIMARY KEY`.
* to adjust it accordingly. For example, in PostgreSQL, it should be `id SERIAL PRIMARY KEY`.
*
* The indexes declared above are not required. They are mainly used to improve the performance
* of some queries about message levels and categories. Depending on your actual needs, you may
* want to create other indexes.
* want to create additional indexes (e.g. index on log_time).
*/
public $tableName = '{{log}}';
public $tableName = 'tbl_log';
private $_db;
/**
* Returns the DB connection used for saving log messages.
* @return \yii\db\Connection the DB connection instance
* @throws \yii\base\Exception if [[connectionID]] does not refer to a valid application component ID.
* @return Connection the DB connection instance
* @throws InvalidConfigException if [[connectionID]] does not point to a valid application component.
*/
public function getDb()
{
if ($this->_db === null) {
$this->_db = \Yii::$application->getComponent($this->connectionID);
if (!$this->_db instanceof \yii\db\Connection) {
throw new \yii\base\Exception('DbTarget.connectionID must refer to a valid application component ID');
$db = \Yii::$application->getComponent($this->connectionID);
if ($db instanceof Connection) {
$this->_db = $db;
} else {
throw new InvalidConfigException("DbTarget::connectionID must refer to the ID of a DB application component.");
}
}
return $this->_db;
}
/**
* Sets the DB connection used by the cache component.
* @param Connection $value the DB connection instance
*/
public function setDb($value)
{
$this->_db = $value;
}
/**
* Stores log [[messages]] to DB.
* @param boolean $final whether this method is called at the end of the current application
*/
public function exportMessages($final)
{
$sql = "INSERT INTO {$this->tableName}
(level, category, log_time, message) VALUES
(:level, :category, :log_time, :message)";
$command = $this->getDb()->createCommand($sql);
$db = $this->getDb();
$tableName = $db->quoteTableName($this->tableName);
$sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)";
$command = $db->createCommand($sql);
foreach ($this->messages as $message) {
$command->bindValues(array(
':level' => $message[1],

47
framework/logging/FileTarget.php

@ -8,17 +8,16 @@
*/
namespace yii\logging;
use yii\base\InvalidConfigException;
/**
* FileTarget records log messages in files.
* FileTarget records log messages in a file.
*
* The log files are stored under [[logPath]] and their name
* is specified by [[logFile]]. If the size of the log file exceeds
* [[maxFileSize]] (in kilo-bytes), a rotation will be performed,
* which renames the current log file by suffixing the file name
* with '.1'. All existing log files are moved backwards one place,
* i.e., '.2' to '.3', '.1' to '.2', and so on. The property
* [[maxLogFiles]] specifies how many files to keep.
* The log file is specified via [[logFile]]. If the size of the log file exceeds
* [[maxFileSize]] (in kilo-bytes), a rotation will be performed, which renames
* the current log file by suffixing the file name with '.1'. All existing log
* files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on.
* The property [[maxLogFiles]] specifies how many files to keep.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
@ -26,6 +25,12 @@ namespace yii\logging;
class FileTarget extends Target
{
/**
* @var string log file path or path alias. If not set, it means the 'application.log' file under
* the application runtime directory. Please make sure the directory containing
* the log file is writable by the Web server process.
*/
public $logFile;
/**
* @var integer maximum log file size, in kilo-bytes. Defaults to 1024, meaning 1MB.
*/
public $maxFileSize = 1024; // in KB
@ -33,14 +38,6 @@ class FileTarget extends Target
* @var integer number of log files used for rotation. Defaults to 5.
*/
public $maxLogFiles = 5;
/**
* @var string directory storing log files. Defaults to the application runtime path.
*/
public $logPath;
/**
* @var string log file name. Defaults to 'application.log'.
*/
public $logFile = 'application.log';
/**
@ -50,11 +47,14 @@ class FileTarget extends Target
public function init()
{
parent::init();
if ($this->logPath === null) {
$this->logPath = \Yii::$application->getRuntimePath();
if ($this->logFile === null) {
$this->logFile = \Yii::$application->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log';
} else {
$this->logFile = \Yii::getAlias($this->logFile);
}
if (!is_dir($this->logPath) || !is_writable($this->logPath)) {
throw new \yii\base\Exception("Directory '{$this->logPath}' does not exist or is not writable.");
$logPath = dirname($this->logFile);
if (!is_dir($logPath) || !is_writable($logPath)) {
throw new InvalidConfigException("Directory '$logPath' does not exist or is not writable.");
}
if ($this->maxLogFiles < 1) {
$this->maxLogFiles = 1;
@ -70,15 +70,14 @@ class FileTarget extends Target
*/
public function exportMessages($final)
{
$logFile = $this->logPath . DIRECTORY_SEPARATOR . $this->logFile;
if (@filesize($logFile) > $this->maxFileSize * 1024) {
if (@filesize($this->logFile) > $this->maxFileSize * 1024) {
$this->rotateFiles();
}
$messages = array();
foreach ($this->messages as $message) {
$messages[] = $this->formatMessage($message);
}
@file_put_contents($logFile, implode('', $messages), FILE_APPEND | LOCK_EX);
@file_put_contents($this->logFile, implode('', $messages), FILE_APPEND | LOCK_EX);
}
/**
@ -86,7 +85,7 @@ class FileTarget extends Target
*/
protected function rotateFiles()
{
$file = $this->logPath . DIRECTORY_SEPARATOR . $this->logFile;
$file = $this->logFile;
for ($i = $this->maxLogFiles; $i > 0; --$i) {
$rotateFile = $file . '.' . $i;
if (is_file($rotateFile)) {

131
framework/logging/Logger.php

@ -10,6 +10,7 @@
namespace yii\logging;
use yii\base\Event;
use yii\base\Exception;
/**
* Logger records logged messages in memory.
@ -18,8 +19,6 @@ use yii\base\Event;
* call [[flush()]] to send logged messages to different log targets, such as
* file, email, Web.
*
* Logger provides a set of events for further customization:
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
@ -30,12 +29,40 @@ class Logger extends \yii\base\Component
*/
const EVENT_FLUSH = 'flush';
const LEVEL_ERROR = 'error';
const LEVEL_WARNING = 'warning';
const LEVEL_INFO = 'info';
const LEVEL_TRACE = 'trace';
const LEVEL_PROFILE_BEGIN = 'profile-begin';
const LEVEL_PROFILE_END = 'profile-end';
/**
* Error message level. An error message is one that indicates the abnormal termination of the
* application and may require developer's handling.
*/
const LEVEL_ERROR = 0x01;
/**
* Warning message level. A warning message is one that indicates some abnormal happens but
* the application is able to continue to run. Developers should pay attention to this message.
*/
const LEVEL_WARNING = 0x02;
/**
* Informational message level. An informational message is one that includes certain information
* for developers to review.
*/
const LEVEL_INFO = 0x04;
/**
* Tracing message level. An tracing message is one that reveals the code execution flow.
*/
const LEVEL_TRACE = 0x08;
/**
* Profiling message level. This indicates the message is for profiling purpose.
*/
const LEVEL_PROFILE = 0x40;
/**
* Profiling message level. This indicates the message is for profiling purpose. It marks the
* beginning of a profiling block.
*/
const LEVEL_PROFILE_BEGIN = 0x50;
/**
* Profiling message level. This indicates the message is for profiling purpose. It marks the
* end of a profiling block.
*/
const LEVEL_PROFILE_END = 0x60;
/**
* @var integer how many messages should be logged before they are flushed from memory and sent to targets.
@ -52,7 +79,7 @@ class Logger extends \yii\base\Component
* ~~~
* array(
* [0] => message (mixed)
* [1] => level (string)
* [1] => level (integer)
* [2] => category (string)
* [3] => timestamp (float, obtained by microtime(true))
* )
@ -61,85 +88,13 @@ class Logger extends \yii\base\Component
public $messages = array();
/**
* Logs an error message.
* An error message is typically logged when an unrecoverable error occurs
* during the execution of an application.
* @param mixed $message the message to be logged.
* @param string $category the category of the message.
*/
public function error($message, $category = 'application')
{
$this->log($message, self::LEVEL_ERROR, $category);
}
/**
* Logs a trace message.
* Trace messages are logged mainly for development purpose to see
* the execution work flow of some code.
* @param mixed $message the message to be logged.
* @param string $category the category of the message.
*/
public function trace($message, $category = 'application')
{
$this->log($message, self::LEVEL_TRACE, $category);
}
/**
* Logs a warning message.
* A warning message is typically logged when an error occurs while the execution
* can still continue.
* @param mixed $message the message to be logged.
* @param string $category the category of the message.
*/
public function warning($message, $category = 'application')
{
$this->log($message, self::LEVEL_WARNING, $category);
}
/**
* Logs an informative message.
* An informative message is typically logged by an application to keep record of
* something important (e.g. an administrator logs in).
* @param mixed $message the message to be logged.
* @param string $category the category of the message.
*/
public function info($message, $category = 'application')
{
$this->log($message, self::LEVEL_INFO, $category);
}
/**
* Marks the beginning of a code block for profiling.
* This has to be matched with a call to [[endProfile()]] with the same category name.
* The begin- and end- calls must also be properly nested.
* @param string $token token for the code block
* @param string $category the category of this log message
* @see endProfile
*/
public function beginProfile($token, $category = 'application')
{
$this->log($token, self::LEVEL_PROFILE_BEGIN, $category);
}
/**
* Marks the end of a code block for profiling.
* This has to be matched with a previous call to [[beginProfile()]] with the same category name.
* @param string $token token for the code block
* @param string $category the category of this log message
* @see beginProfile
*/
public function endProfile($token, $category = 'application')
{
$this->log($token, self::LEVEL_PROFILE_END, $category);
}
/**
* Logs a message with the given type and category.
* If `YII_DEBUG` is true and `YII_TRACE_LEVEL` is greater than 0, then additional
* call stack information about application code will be appended to the message.
* @param string $message the message to be logged.
* @param string $level the level of the message. This must be one of the following:
* 'trace', 'info', 'warning', 'error', 'profile'.
* @param integer $level the level of the message. This must be one of the following:
* `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`,
* `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`.
* @param string $category the category of the message.
*/
public function log($message, $level, $category = 'application')
@ -242,14 +197,14 @@ class Logger extends \yii\base\Component
$stack = array();
foreach ($this->messages as $log) {
if ($log[1] === self::LEVEL_PROFILE_BEGIN) {
list($token, $level, $category, $timestamp) = $log;
if ($level == self::LEVEL_PROFILE_BEGIN) {
$stack[] = $log;
} elseif ($log[1] === self::LEVEL_PROFILE_END) {
list($token, $level, $category, $timestamp) = $log;
} elseif ($level == self::LEVEL_PROFILE_END) {
if (($last = array_pop($stack)) !== null && $last[0] === $token) {
$timings[] = array($token, $category, $timestamp - $last[3]);
} else {
throw new \yii\base\Exception("Unmatched profiling block: $token");
throw new Exception("Unmatched profiling block: $token");
}
}
}

26
framework/logging/Router.php

@ -9,6 +9,10 @@
namespace yii\logging;
use Yii;
use yii\base\Component;
use yii\base\Application;
/**
* Router manages [[Target|log targets]] that record log messages in different media.
*
@ -48,17 +52,17 @@ namespace yii\logging;
* as follows:
*
* ~~~
* \Yii::$application->log->targets['file']->enabled = false;
* Yii::$application->log->targets['file']->enabled = false;
* ~~~
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Router extends \yii\base\ApplicationComponent
class Router extends Component
{
/**
* @var Target[] list of log target objects or configurations. If the latter, target objects will
* be created in [[init()]] by calling [[\Yii::createObject()]] with the corresponding object configuration.
* be created in [[init()]] by calling [[Yii::createObject()]] with the corresponding object configuration.
*/
public $targets = array();
@ -74,28 +78,28 @@ class Router extends \yii\base\ApplicationComponent
foreach ($this->targets as $name => $target) {
if (!$target instanceof Target) {
$this->targets[$name] = \Yii::createObject($target);
$this->targets[$name] = Yii::createObject($target);
}
}
\Yii::getLogger()->on('flush', array($this, 'processMessages'));
if (\Yii::$application !== null) {
\Yii::$application->on('afterRequest', array($this, 'processMessages'));
Yii::getLogger()->on(Logger::EVENT_FLUSH, array($this, 'processMessages'));
if (Yii::$application !== null) {
Yii::$application->on(Application::EVENT_AFTER_REQUEST, array($this, 'processMessages'));
}
}
/**
* Retrieves and processes log messages from the system logger.
* This method mainly serves the event handler to [[Logger::onFlush]]
* and [[\yii\base\Application::onEndRequest]] events.
* It will retrieve the available log messages from the [[\Yii::getLogger|system logger]]
* and [[Application::onEndRequest]] events.
* It will retrieve the available log messages from the [[Yii::getLogger|system logger]]
* and invoke the registered [[targets|log targets]] to do the actual processing.
* @param \yii\base\Event $event event parameter
*/
public function processMessages($event)
{
$messages = \Yii::getLogger()->messages;
$final = $event->name !== 'flush';
$messages = Yii::getLogger()->messages;
$final = $event->name === Application::EVENT_AFTER_REQUEST;
foreach ($this->targets as $target) {
if ($target->enabled) {
$target->processMessages($messages, $final);

93
framework/logging/Target.php

@ -9,6 +9,8 @@
namespace yii\logging;
use yii\base\InvalidConfigException;
/**
* Target is the base class for all log target classes.
*
@ -20,6 +22,8 @@ namespace yii\logging;
* satisfying both filter conditions will be handled. Additionally, you
* may specify [[except]] to exclude messages of certain categories.
*
* @property integer $levels the message levels that this target is interested in.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
@ -30,10 +34,6 @@ abstract class Target extends \yii\base\Component
*/
public $enabled = true;
/**
* @var array list of message levels that this target is interested in. Defaults to empty, meaning all levels.
*/
public $levels = array();
/**
* @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories.
* You can use an asterisk at the end of a category so that the category may be used to
* match those categories sharing the same common prefix. For example, 'yii\db\*' will match
@ -70,17 +70,19 @@ abstract class Target extends \yii\base\Component
*/
public $logVars = array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER');
/**
* @var boolean whether this target should export the collected messages to persistent storage
* (e.g. DB, email) whenever [[processMessages()]] is called. Defaults to true. If false,
* the collected messages will be stored in [[messages]] without any further processing.
* @var integer how many messages should be accumulated before they are exported.
* Defaults to 1000. Note that messages will always be exported when the application terminates.
* Set this property to be 0 if you don't want to export messages until the application terminates.
*/
public $autoExport = true;
public $exportInterval = 1000;
/**
* @var array the messages that are retrieved from the logger so far by this log target.
* @see autoExport
*/
public $messages = array();
private $_levels = 0;
/**
* Exports log messages to a specific destination.
* Child classes must implement this method. Note that you may need
@ -102,7 +104,8 @@ abstract class Target extends \yii\base\Component
$messages = $this->filterMessages($messages);
$this->messages = array_merge($this->messages, $messages);
if (!empty($this->messages) && ($this->autoExport || $final)) {
$count = count($this->messages);
if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) {
$this->prepareExport($final);
$this->exportMessages($final);
$this->messages = array();
@ -160,6 +163,58 @@ abstract class Target extends \yii\base\Component
}
/**
* @return integer the message levels that this target is interested in. This is a bitmap of
* level values. Defaults to 0, meaning all available levels.
*/
public function getLevels()
{
return $this->_levels;
}
/**
* Sets the message levels that this target is interested in.
*
* The parameter can be either an array of interested level names or an integer representing
* the bitmap of the interested level values. Valid level names include: 'error',
* 'warning', 'info', 'trace' and 'profile'; valid level values include:
* [[Logger::LEVEL_ERROR]], [[Logger::LEVEL_WARNING]], [[Logger::LEVEL_INFO]],
* [[Logger::LEVEL_TRACE]] and [[Logger::LEVEL_PROFILE]].
*
* For example,
*
* ~~~
* array('error', 'warning')
* // which is equivalent to:
* Logger::LEVEL_ERROR | Logger::LEVEL_WARNING
* ~~~
*
* @param array|integer $levels message levels that this target is interested in.
* @throws InvalidConfigException if an unknown level name is given
*/
public function setLevels($levels)
{
static $levelMap = array(
'error' => Logger::LEVEL_ERROR,
'warning' => Logger::LEVEL_WARNING,
'info' => Logger::LEVEL_INFO,
'trace' => Logger::LEVEL_TRACE,
'profile' => Logger::LEVEL_PROFILE,
);
if (is_array($levels)) {
$this->_levels = 0;
foreach ($levels as $level) {
if (isset($levelMap[$level])) {
$this->_levels |= $levelMap[$level];
} else {
throw new InvalidConfigException("Unrecognized level: $level");
}
}
} else {
$this->_levels = $levels;
}
}
/**
* Filters the given messages according to their categories and levels.
* @param array $messages messages to be filtered
* @return array the filtered messages.
@ -168,8 +223,10 @@ abstract class Target extends \yii\base\Component
*/
protected function filterMessages($messages)
{
$levels = $this->getLevels();
foreach ($messages as $i => $message) {
if (!empty($this->levels) && !in_array($message[1], $this->levels)) {
if ($levels && !($levels & $message[1])) {
unset($messages[$i]);
continue;
}
@ -210,7 +267,19 @@ abstract class Target extends \yii\base\Component
*/
public function formatMessage($message)
{
$s = is_string($message[0]) ? $message[0] : var_export($message[0], true);
return date('Y/m/d H:i:s', $message[3]) . " [{$message[1]}] [{$message[2]}] $s\n";
static $levels = array(
Logger::LEVEL_ERROR => 'error',
Logger::LEVEL_WARNING => 'warning',
Logger::LEVEL_INFO => 'info',
Logger::LEVEL_TRACE => 'trace',
Logger::LEVEL_PROFILE_BEGIN => 'profile begin',
Logger::LEVEL_PROFILE_END => 'profile end',
);
list($text, $level, $category, $timestamp) = $message;
$level = isset($levels[$level]) ? $levels[$level] : 'unknown';
if (!is_string($text)) {
$text = var_export($text, true);
}
return date('Y/m/d H:i:s', $timestamp) . " [$level] [$category] $text\n";
}
}

23
todo.md

@ -1,6 +1,18 @@
- db
* pgsql, sql server, oracle, db2 drivers
* write a guide on creating own schema definitions
* document-based (should allow storage-specific methods additionally to generic ones)
* mongodb (put it under framework/db/mongodb)
* key-value-based (should allow storage-specific methods additionally to generic ones)
* redis (put it under framework/db/redis or perhaps framework/caching?)
- logging
* WebTarget
* ProfileTarget
- caching
* a console command to clear cached data
---
- base
* module
- Module should be able to define its own configuration including routes. Application should be able to overwrite it.
@ -16,17 +28,6 @@
* support for markdown syntax
* support for [[name]]
* consider to be released as a separate tool for user app docs
- caching
* a way to invalidate/clear cached data
* a command to clear cached data
- db
* pgsql, sql server, oracle, db2 drivers
* write a guide on creating own schema definitions
* document-based (should allow storage-specific methods additionally to generic ones)
* mongodb
* key-value-based (should allow storage-specific methods additionally to generic ones)
* redis
* memcachedb
- i18n
* consider using PHP built-in support and data
* message translations, choice format

Loading…
Cancel
Save