diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 76a6e0b..a0f20c4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,6 +4,7 @@ services:
- docker:dind
variables:
+ DOCKER_YII2_PHP_IMAGE: yiisoftware/yii2-php:7.1-apache
DOCKER_MYSQL_IMAGE: percona:5.7
DOCKER_POSTGRES_IMAGE: postgres:9.3
diff --git a/.travis.yml b/.travis.yml
index 3efb1ef..741ad24 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -46,9 +46,14 @@ cache:
- $HOME/.composer/cache
- $HOME/.npm
-# try running against postgres 9.3
+# try running against postgres 9.6
addons:
- postgresql: "9.3"
+ postgresql: "9.6"
+ apt:
+ sources:
+ - mysql-5.7-trusty
+ packages:
+ - mysql-server
code_climate:
repo_token: 2935307212620b0e2228ab67eadd92c9f5501ddb60549d0d86007a354d56915b
@@ -119,6 +124,7 @@ before_script:
- |
if [ $TASK_TESTS_PHP == 1 ]; then
travis_retry mysql -e 'CREATE DATABASE `yiitest`;';
+ mysql -e "SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';";
mysql -e "CREATE USER 'travis'@'localhost' IDENTIFIED WITH mysql_native_password;";
mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'travis'@'localhost' WITH GRANT OPTION;";
psql -U postgres -c 'CREATE DATABASE yiitest;';
diff --git a/Dockerfile b/Dockerfile
index 88761db..77ffdfd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,5 @@
-FROM yiisoftware/yii2-php:7.1-apache
+ARG DOCKER_YII2_PHP_IMAGE
+FROM ${DOCKER_YII2_PHP_IMAGE}
# Project source-code
WORKDIR /project
diff --git a/README.md b/README.md
index 2bc59f1..40cf36e 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@ Yii 2 is a modern framework designed to be a solid foundation for your PHP appli
It is fast, secure and efficient and works right out of the box pre-configured with reasonable defaults.
The framework is easy to adjust to meet your needs, because Yii has been designed to be flexible.
-[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2/v/stable.png)](https://packagist.org/packages/yiisoft/yii2)
-[![Total Downloads](https://poser.pugx.org/yiisoft/yii2/downloads.png)](https://packagist.org/packages/yiisoft/yii2)
+[![Latest Stable Version](https://img.shields.io/packagist/v/yiisoft/yii2.svg)](https://packagist.org/packages/yiisoft/yii2)
+[![Total Downloads](https://img.shields.io/packagist/dt/yiisoft/yii2.svg)](https://packagist.org/packages/yiisoft/yii2)
[![Build Status](https://img.shields.io/travis/yiisoft/yii2.svg)](http://travis-ci.org/yiisoft/yii2)
[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/coverage.png?s=31d80f1036099e9d6a3e4d7738f6b000b3c3d10e)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/yiisoft/yii2/badges/quality-score.png?s=b1074a1ff6d0b214d54fa5ab7abbb90fc092471d)](https://scrutinizer-ci.com/g/yiisoft/yii2/)
diff --git a/build/controllers/ClassmapController.php b/build/controllers/ClassmapController.php
index a88e6cd..7a4e3b2 100644
--- a/build/controllers/ClassmapController.php
+++ b/build/controllers/ClassmapController.php
@@ -60,7 +60,7 @@ class ClassmapController extends Controller
if (strpos($file, $root) !== 0) {
throw new Exception("Something wrong: $file\n");
}
- $path = str_replace('\\', '/', substr($file, strlen($root)));
+ $path = str_replace('\\', '/', substr($file, \strlen($root)));
$map[$path] = " 'yii" . substr(str_replace('/', '\\', $path), 0, -4) . "' => YII2_PATH . '$path',";
}
ksort($map);
diff --git a/build/controllers/DevController.php b/build/controllers/DevController.php
index 53fa402..5d64da5 100644
--- a/build/controllers/DevController.php
+++ b/build/controllers/DevController.php
@@ -24,19 +24,25 @@ use yii\helpers\FileHelper;
*/
class DevController extends Controller
{
+ /**
+ * {@inheritdoc}
+ */
public $defaultAction = 'all';
-
/**
* @var bool whether to use HTTP when cloning github repositories
*/
public $useHttp = false;
-
+ /**
+ * @var array
+ */
public $apps = [
'basic' => 'git@github.com:yiisoft/yii2-app-basic.git',
'advanced' => 'git@github.com:yiisoft/yii2-app-advanced.git',
'benchmark' => 'git@github.com:yiisoft/yii2-app-benchmark.git',
];
-
+ /**
+ * @var array
+ */
public $extensions = [
'apidoc' => 'git@github.com:yiisoft/yii2-apidoc.git',
'authclient' => 'git@github.com:yiisoft/yii2-authclient.git',
@@ -98,17 +104,17 @@ class DevController extends Controller
*/
public function actionRun($command)
{
- $command = implode(' ', func_get_args());
+ $command = implode(' ', \func_get_args());
// root of the dev repo
- $base = dirname(dirname(__DIR__));
+ $base = \dirname(\dirname(__DIR__));
$dirs = $this->listSubDirs("$base/extensions");
$dirs = array_merge($dirs, $this->listSubDirs("$base/apps"));
asort($dirs);
$oldcwd = getcwd();
foreach ($dirs as $dir) {
- $displayDir = substr($dir, strlen($base));
+ $displayDir = substr($dir, \strlen($base));
$this->stdout("Running '$command' in $displayDir...\n", Console::BOLD);
chdir($dir);
passthru($command);
@@ -137,7 +143,7 @@ class DevController extends Controller
public function actionApp($app, $repo = null)
{
// root of the dev repo
- $base = dirname(dirname(__DIR__));
+ $base = \dirname(\dirname(__DIR__));
$appDir = "$base/apps/$app";
if (!file_exists($appDir)) {
@@ -188,7 +194,7 @@ class DevController extends Controller
public function actionExt($extension, $repo = null)
{
// root of the dev repo
- $base = dirname(dirname(__DIR__));
+ $base = \dirname(\dirname(__DIR__));
$extensionDir = "$base/extensions/$extension";
if (!file_exists($extensionDir)) {
@@ -229,12 +235,12 @@ class DevController extends Controller
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function options($actionID)
{
$options = parent::options($actionID);
- if (in_array($actionID, ['ext', 'app', 'all'], true)) {
+ if (\in_array($actionID, ['ext', 'app', 'all'], true)) {
$options[] = 'useHttp';
}
diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php
index 1d3fea1..5d894ca 100644
--- a/build/controllers/PhpDocController.php
+++ b/build/controllers/PhpDocController.php
@@ -11,6 +11,7 @@ use Yii;
use yii\console\Controller;
use yii\helpers\Console;
use yii\helpers\FileHelper;
+use yii\helpers\Json;
/**
* PhpDocController is there to help maintaining PHPDoc annotation in class files.
@@ -21,6 +22,9 @@ use yii\helpers\FileHelper;
*/
class PhpDocController extends Controller
{
+ /**
+ * {@inheritdoc}
+ */
public $defaultAction = 'property';
/**
* @var bool whether to update class docs directly. Setting this to false will just output docs
@@ -82,7 +86,7 @@ class PhpDocController extends Controller
$nFilesUpdated = 0;
foreach ($files as $file) {
$contents = file_get_contents($file);
- $sha = sha1($contents);
+ $hash = $this->hash($contents);
// fix line endings
$lines = preg_split('/(\r\n|\n|\r)/', $contents);
@@ -94,10 +98,10 @@ class PhpDocController extends Controller
$lines = array_values($this->fixLineSpacing($lines));
$newContent = implode("\n", $lines);
- if ($sha !== sha1($newContent)) {
+ if ($hash !== $this->hash($newContent)) {
+ file_put_contents($file, $newContent);
$nFilesUpdated++;
}
- file_put_contents($file, $newContent);
$nFilesTotal++;
}
@@ -106,13 +110,18 @@ class PhpDocController extends Controller
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function options($actionID)
{
return array_merge(parent::options($actionID), ['updateFiles', 'skipFrameworkRequirements']);
}
+ /**
+ * @param string $root
+ * @param bool $needsInclude
+ * @return array list of files.
+ */
protected function findFiles($root, $needsInclude = true)
{
$except = [];
@@ -132,7 +141,7 @@ class PhpDocController extends Controller
'/generators/extension/default/AutoloadExample.php',
],
'swiftmailer' => [
- '/Logger.php',
+ 'src/Logger.php',
],
'twig' => [
'/Extension.php',
@@ -147,13 +156,9 @@ class PhpDocController extends Controller
}
if ($root === null) {
- $root = dirname(YII2_PATH);
+ $root = \dirname(YII2_PATH);
$extensionPath = "$root/extensions";
- foreach (scandir($extensionPath) as $extension) {
- if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
- Yii::setAlias("@yii/$extension", "$extensionPath/$extension");
- }
- }
+ $this->setUpExtensionAliases($extensionPath);
$except = [
'/apps/',
@@ -172,11 +177,11 @@ class PhpDocController extends Controller
}
}
} elseif (preg_match('~extensions/([\w-]+)[\\\\/]?$~', $root, $matches)) {
- $extensionPath = dirname(rtrim($root, '\\/'));
+ $extensionPath = \dirname(rtrim($root, '\\/'));
$this->setUpExtensionAliases($extensionPath);
- $extension = $matches[1];
- Yii::setAlias("@yii/$extension", "$root");
+ [, $extension] = $matches;
+ Yii::setAlias("@yii/$extension", (string)$root);
if (is_file($autoloadFile = Yii::getAlias("@yii/$extension/vendor/autoload.php"))) {
include $autoloadFile;
}
@@ -195,11 +200,11 @@ class PhpDocController extends Controller
// return [];
// }
} elseif (preg_match('~apps/([\w-]+)[\\\\/]?$~', $root, $matches)) {
- $extensionPath = dirname(dirname(rtrim($root, '\\/'))) . '/extensions';
+ $extensionPath = \dirname(\dirname(rtrim($root, '\\/'))) . '/extensions';
$this->setUpExtensionAliases($extensionPath);
- $appName = $matches[1];
- Yii::setAlias("@app-$appName", "$root");
+ [, $appName] = $matches;
+ Yii::setAlias("@app-$appName", (string)$root);
if (is_file($autoloadFile = Yii::getAlias("@app-$appName/vendor/autoload.php"))) {
include $autoloadFile;
}
@@ -230,14 +235,30 @@ class PhpDocController extends Controller
'vendor/',
]),
];
+
return FileHelper::findFiles($root, $options);
}
+ /**
+ * @param string $extensionPath root path containing extension repositories.
+ */
private function setUpExtensionAliases($extensionPath)
{
foreach (scandir($extensionPath) as $extension) {
if (ctype_alpha($extension) && is_dir($extensionPath . '/' . $extension)) {
Yii::setAlias("@yii/$extension", "$extensionPath/$extension");
+
+ $composerConfigFile = $extensionPath . '/' . $extension . '/composer.json';
+ if (file_exists($composerConfigFile)) {
+ $composerConfig = Json::decode(file_get_contents($composerConfigFile));
+ if (isset($composerConfig['autoload']['psr-4'])) {
+ foreach ($composerConfig['autoload']['psr-4'] as $namespace => $subPath) {
+ $alias = '@' . str_replace('\\', '/', $namespace);
+ $path = rtrim("$extensionPath/$extension/$subPath", '/');
+ Yii::setAlias($alias, $path);
+ }
+ }
+ }
}
}
}
@@ -285,6 +306,7 @@ class PhpDocController extends Controller
/**
* Markdown aware fix of whitespace issues in doc comments.
+ * @param array $lines
*/
protected function fixDocBlockIndentation(&$lines)
{
@@ -326,7 +348,7 @@ class PhpDocController extends Controller
$codeBlock = !$codeBlock;
$listIndent = '';
} elseif (preg_match('/^(\s*)([0-9]+\.|-|\*|\+) /', $docLine, $matches)) {
- $listIndent = str_repeat(' ', strlen($matches[0]));
+ $listIndent = str_repeat(' ', \strlen($matches[0]));
$tag = false;
$lines[$i] = $indent . ' * ' . $docLine;
continue;
@@ -340,6 +362,10 @@ class PhpDocController extends Controller
}
}
+ /**
+ * @param string $line
+ * @return string
+ */
protected function fixParamTypes($line)
{
return preg_replace_callback('~@(param|return) ([\w\\|]+)~i', function ($matches) {
@@ -532,10 +558,10 @@ class PhpDocController extends Controller
if (trim($oldDoc) != trim($newDoc)) {
$fileContent = explode("\n", file_get_contents($file));
$start = $ref->getStartLine() - 2;
- $docStart = $start - count(explode("\n", $oldDoc)) + 1;
+ $docStart = $start - \count(explode("\n", $oldDoc)) + 1;
$newFileContent = [];
- $n = count($fileContent);
+ $n = \count($fileContent);
for ($i = 0; $i < $n; $i++) {
if ($i > $start || $i < $docStart) {
$newFileContent[] = $fileContent[$i];
@@ -562,7 +588,7 @@ class PhpDocController extends Controller
protected function cleanDocComment($doc)
{
$lines = explode("\n", $doc);
- $n = count($lines);
+ $n = \count($lines);
for ($i = 0; $i < $n; $i++) {
$lines[$i] = rtrim($lines[$i]);
if (trim($lines[$i]) == '*' && trim($lines[$i + 1]) == '*') {
@@ -603,7 +629,7 @@ class PhpDocController extends Controller
// if no properties or other tags where present add properties at the end
if ($propertyPosition === false) {
- $propertyPosition = count($lines) - 2;
+ $propertyPosition = \count($lines) - 2;
}
$finalDoc = '';
@@ -626,22 +652,22 @@ class PhpDocController extends Controller
$namespace = $namespace['name'];
$classes = $this->match('#\n(?:abstract )?class (?\w+)( extends .+)?( implements .+)?\n\{(?.*)\n\}(\n|$)#', $file);
- if (count($classes) > 1) {
+ if (\count($classes) > 1) {
$this->stderr("[ERR] There should be only one class in a file: $fileName\n", Console::FG_RED);
return false;
}
- if (count($classes) < 1) {
+ if (\count($classes) < 1) {
$interfaces = $this->match('#\ninterface (?\w+)( extends .+)?\n\{(?.*)\n\}(\n|$)#', $file);
- if (count($interfaces) == 1) {
+ if (\count($interfaces) == 1) {
return false;
- } elseif (count($interfaces) > 1) {
+ } elseif (\count($interfaces) > 1) {
$this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED);
} else {
$traits = $this->match('#\ntrait (?\w+)\n\{(?.*)\n\}(\n|$)#', $file);
- if (count($traits) == 1) {
+ if (\count($traits) == 1) {
return false;
- } elseif (count($traits) > 1) {
+ } elseif (\count($traits) > 1) {
$this->stderr("[ERR] There should be only one class/trait/interface in a file: $fileName\n", Console::FG_RED);
} else {
$this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED);
@@ -682,13 +708,13 @@ class PhpDocController extends Controller
ksort($props);
- if (count($props) > 0) {
+ if (\count($props) > 0) {
$phpdoc .= " *\n";
foreach ($props as $propName => &$prop) {
$docline = ' * @';
$docline .= 'property'; // Do not use property-read and property-write as few IDEs support complex syntax.
$note = '';
- if (isset($prop['get']) && isset($prop['set'])) {
+ if (isset($prop['get'], $prop['set'])) {
if ($prop['get']['type'] != $prop['set']['type']) {
$note = ' Note that the type of this property differs in getter and setter.'
. ' See [[get' . ucfirst($propName) . '()]] and [[set' . ucfirst($propName) . '()]] for details.';
@@ -706,7 +732,7 @@ class PhpDocController extends Controller
}
if (!$parentSetter) {
$note = ' This property is read-only.';
-// $docline .= '-read';
+ //$docline .= '-read';
}
} elseif (isset($prop['set'])) {
// check if parent class has getter defined
@@ -721,7 +747,7 @@ class PhpDocController extends Controller
}
if (!$parentGetter) {
$note = ' This property is write-only.';
-// $docline .= '-write';
+ //$docline .= '-write';
}
} else {
continue;
@@ -773,11 +799,24 @@ class PhpDocController extends Controller
return '';
}
- return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[strlen($str) - 1] != '.' ? '.' : '');
+ return strtoupper(substr($str, 0, 1)) . substr($str, 1) . ($str[\strlen($str) - 1] != '.' ? '.' : '');
}
protected function getPropParam($prop, $param)
{
return isset($prop['property']) ? $prop['property'][$param] : (isset($prop['get']) ? $prop['get'][$param] : $prop['set'][$param]);
}
+
+ /**
+ * Generate a hash value (message digest)
+ * @param string $string message to be hashed.
+ * @return string calculated message digest.
+ */
+ private function hash($string)
+ {
+ if (!function_exists('hash')) {
+ return sha1($string);
+ }
+ return hash('sha256', $string);
+ }
}
diff --git a/build/controllers/ReleaseController.php b/build/controllers/ReleaseController.php
index 32f5c1a..336c46c 100644
--- a/build/controllers/ReleaseController.php
+++ b/build/controllers/ReleaseController.php
@@ -82,7 +82,7 @@ class ReleaseController extends Controller
throw new Exception('Sorry, but releases should be run interactively to ensure you actually verify what you are doing ;)');
}
if ($this->basePath === null) {
- $this->basePath = dirname(dirname(__DIR__));
+ $this->basePath = \dirname(\dirname(__DIR__));
}
$this->basePath = rtrim($this->basePath, '\\/');
return parent::beforeAction($action);
@@ -109,7 +109,7 @@ class ReleaseController extends Controller
foreach ($items as $item) {
$this->stdout("fetching tags for $item...");
if ($item === 'framework') {
- $this->gitFetchTags("{$this->basePath}");
+ $this->gitFetchTags((string)$this->basePath);
} elseif (strncmp('app-', $item, 4) === 0) {
$this->gitFetchTags("{$this->basePath}/apps/" . substr($item, 4));
} else {
@@ -183,7 +183,7 @@ class ReleaseController extends Controller
*/
public function actionRelease(array $what)
{
- if (count($what) > 1) {
+ if (\count($what) > 1) {
$this->stdout("Currently only one simultaneous release is supported.\n");
return 1;
}
@@ -315,7 +315,7 @@ class ReleaseController extends Controller
*/
public function actionSortChangelog(array $what)
{
- if (count($what) > 1) {
+ if (\count($what) > 1) {
$this->stdout("Currently only one simultaneous release is supported.\n");
return 1;
}
@@ -376,7 +376,7 @@ class ReleaseController extends Controller
{
foreach ($what as $w) {
if (strncmp('app-', $w, 4) === 0) {
- if (!empty($limit) && !in_array('app', $limit)) {
+ if (!empty($limit) && !\in_array('app', $limit)) {
throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
}
if (!is_dir($appPath = "{$this->basePath}/apps/" . substr($w, 4))) {
@@ -386,7 +386,7 @@ class ReleaseController extends Controller
$this->ensureGitClean($appPath);
}
} elseif ($w === 'framework') {
- if (!empty($limit) && !in_array('framework', $limit)) {
+ if (!empty($limit) && !\in_array('framework', $limit)) {
throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
}
if (!is_dir($fwPath = "{$this->basePath}/framework")) {
@@ -396,7 +396,7 @@ class ReleaseController extends Controller
$this->ensureGitClean($fwPath);
}
} else {
- if (!empty($limit) && !in_array('ext', $limit)) {
+ if (!empty($limit) && !\in_array('ext', $limit)) {
throw new Exception('Only the following types are allowed: ' . implode(', ', $limit) . "\n");
}
if (!is_dir($extPath = "{$this->basePath}/extensions/$w")) {
@@ -414,7 +414,7 @@ class ReleaseController extends Controller
{
$this->stdout("\n");
$this->stdout($h = "Preparing framework release version $version", Console::BOLD);
- $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
+ $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
exit(1);
@@ -552,7 +552,7 @@ class ReleaseController extends Controller
{
$this->stdout("\n");
$this->stdout($h = "Preparing release for application $name version $version", Console::BOLD);
- $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
+ $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
exit(1);
@@ -670,7 +670,7 @@ class ReleaseController extends Controller
{
$this->stdout("\n");
$this->stdout($h = "Preparing release for extension $name version $version", Console::BOLD);
- $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD);
+ $this->stdout("\n" . str_repeat('-', \strlen($h)) . "\n\n", Console::BOLD);
if (!$this->confirm('Make sure you are on the right branch for this release and that it tracks the correct remote branch! Continue?')) {
exit(1);
@@ -795,7 +795,11 @@ class ReleaseController extends Controller
protected function gitFetchTags($path)
{
- chdir($path);
+ try {
+ chdir($path);
+ } catch (\yii\base\ErrorException $e) {
+ throw new Exception('Failed to getch git tags in ' . $path . ': ' . $e->getMessage());
+ }
exec('git fetch --tags', $output, $ret);
if ($ret != 0) {
throw new Exception('Command "git fetch --tags" failed with code ' . $ret);
@@ -817,7 +821,7 @@ class ReleaseController extends Controller
$headline = $version . ' ' . date('F d, Y');
$this->sed(
'/' . $v . ' under development\n(-+?)\n/',
- $headline . "\n" . str_repeat('-', strlen($headline)) . "\n",
+ $headline . "\n" . str_repeat('-', \strlen($headline)) . "\n",
$this->getChangelogs($what)
);
}
@@ -825,7 +829,7 @@ class ReleaseController extends Controller
protected function openChangelogs($what, $version)
{
$headline = "\n$version under development\n";
- $headline .= str_repeat('-', strlen($headline) - 2) . "\n\n- no changes in this release.\n";
+ $headline .= str_repeat('-', \strlen($headline) - 2) . "\n\n- no changes in this release.\n";
foreach ($this->getChangelogs($what) as $file) {
$lines = explode("\n", file_get_contents($file));
$hl = [
@@ -874,7 +878,7 @@ class ReleaseController extends Controller
$state = 'end';
}
// add continued lines to the last item to keep them together
- if (!empty(${$state}) && trim($line !== '') && strpos($line, '- ') !== 0) {
+ if (!empty(${$state}) && trim($line) !== '' && strncmp($line, '- ', 2) !== 0) {
end(${$state});
${$state}[key(${$state})] .= "\n" . $line;
} else {
@@ -919,7 +923,7 @@ class ReleaseController extends Controller
protected function getChangelogs($what)
{
$changelogs = [];
- if (in_array('framework', $what)) {
+ if (\in_array('framework', $what)) {
$changelogs[] = $this->getFrameworkChangelog();
}
@@ -947,13 +951,13 @@ class ReleaseController extends Controller
protected function composerSetStability($what, $version)
{
$apps = [];
- if (in_array('app-advanced', $what)) {
+ if (\in_array('app-advanced', $what)) {
$apps[] = $this->basePath . '/apps/advanced/composer.json';
}
- if (in_array('app-basic', $what)) {
+ if (\in_array('app-basic', $what)) {
$apps[] = $this->basePath . '/apps/basic/composer.json';
}
- if (in_array('app-benchmark', $what)) {
+ if (\in_array('app-benchmark', $what)) {
$apps[] = $this->basePath . '/apps/benchmark/composer.json';
}
if (empty($apps)) {
diff --git a/build/controllers/Utf8Controller.php b/build/controllers/Utf8Controller.php
index 64e8c0a..ca5175d 100644
--- a/build/controllers/Utf8Controller.php
+++ b/build/controllers/Utf8Controller.php
@@ -29,7 +29,7 @@ class Utf8Controller extends Controller
public function actionCheckGuide($directory = null)
{
if ($directory === null) {
- $directory = dirname(dirname(__DIR__)) . '/docs';
+ $directory = \dirname(\dirname(__DIR__)) . '/docs';
}
if (is_file($directory)) {
$files = [$directory];
@@ -90,7 +90,7 @@ class Utf8Controller extends Controller
}
$hexcode = dechex($this->unicodeOrd($char));
- $hexcode = str_repeat('0', max(4 - strlen($hexcode), 0)) . $hexcode;
+ $hexcode = str_repeat('0', max(4 - \strlen($hexcode), 0)) . $hexcode;
$this->stdout(" at $line:$pos FOUND $what: 0x$hexcode '$char' http://unicode-table.com/en/$hexcode/\n");
}
@@ -105,20 +105,20 @@ class Utf8Controller extends Controller
*/
private function unicodeOrd($c)
{
- $h = ord($c[0]);
+ $h = \ord($c[0]);
if ($h <= 0x7F) {
return $h;
} elseif ($h < 0xC2) {
return false;
} elseif ($h <= 0xDF) {
- return ($h & 0x1F) << 6 | (ord($c[1]) & 0x3F);
+ return ($h & 0x1F) << 6 | (\ord($c[1]) & 0x3F);
} elseif ($h <= 0xEF) {
- return ($h & 0x0F) << 12 | (ord($c[1]) & 0x3F) << 6
- | (ord($c[2]) & 0x3F);
+ return ($h & 0x0F) << 12 | (\ord($c[1]) & 0x3F) << 6
+ | (\ord($c[2]) & 0x3F);
} elseif ($h <= 0xF4) {
- return ($h & 0x0F) << 18 | (ord($c[1]) & 0x3F) << 12
- | (ord($c[2]) & 0x3F) << 6
- | (ord($c[3]) & 0x3F);
+ return ($h & 0x0F) << 18 | (\ord($c[1]) & 0x3F) << 12
+ | (\ord($c[2]) & 0x3F) << 6
+ | (\ord($c[3]) & 0x3F);
}
return false;
diff --git a/docs/guide-es/concept-events.md b/docs/guide-es/concept-events.md
index 2eee4ce..d39979d 100644
--- a/docs/guide-es/concept-events.md
+++ b/docs/guide-es/concept-events.md
@@ -218,7 +218,7 @@ use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' is inserted');
+ Yii::debug(get_class($event->sender) . ' is inserted');
});
```
diff --git a/docs/guide-es/db-migrations.md b/docs/guide-es/db-migrations.md
index c71fcf3..2c44709 100644
--- a/docs/guide-es/db-migrations.md
+++ b/docs/guide-es/db-migrations.md
@@ -204,7 +204,7 @@ esto genera
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -214,7 +214,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -238,7 +238,7 @@ genera
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -250,7 +250,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -275,7 +275,7 @@ genera
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -287,7 +287,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -320,7 +320,7 @@ genera
class m160328_040430_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -368,7 +368,7 @@ class m160328_040430_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -522,7 +522,7 @@ genera
class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -569,7 +569,7 @@ class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migra
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
diff --git a/docs/guide-es/runtime-logging.md b/docs/guide-es/runtime-logging.md
index a09ec93..bb01a21 100644
--- a/docs/guide-es/runtime-logging.md
+++ b/docs/guide-es/runtime-logging.md
@@ -20,7 +20,7 @@ En esta sección, se describirán principalmente los dos primeros pasos.
Registrar mensajes de anotación es tan simple como llamar a uno de los siguientes métodos de registro de anotaciones.
-* [[Yii::trace()]]: registra un mensaje para trazar el funcionamiento de una sección de código. Se usa principalmente
+* [[Yii::debug()]]: registra un mensaje para trazar el funcionamiento de una sección de código. Se usa principalmente
para tareas de desarrollo.
* [[Yii::info()]]: registra un mensaje que transmite información útil.
* [[Yii::warning()]]: registra un mensaje de advertencia que indica que ha sucedido algo inesperado.
@@ -32,7 +32,7 @@ tiene que ser registrado, mientras que `$category` es la categoría del registro
ejemplo registra la huella del mensaje para la categoría `application`:
```php
-Yii::trace('start calculating average revenue');
+Yii::debug('start calculating average revenue');
```
> Info: Los mensajes de registro pueden ser tanto cadenas de texto como datos complejos, como arrays u objetos.
@@ -47,7 +47,7 @@ efectiva de organizarlos es usar la constante predefinida (magic constant) de PH
categoría. Además este es el enfoque que se usa en el código del núcleo (core) del framework Yii. Por ejemplo,
```php
-Yii::trace('start calculating average revenue', __METHOD__);
+Yii::debug('start calculating average revenue', __METHOD__);
```
La constante `__METHOD__` equivale al nombre del método (con el prefijo del nombre completo del nombre de clase) donde
@@ -131,7 +131,7 @@ La propiedad [[yii\log\Target::levels|levels]] es un array que consta de uno o v
* `error`: correspondiente a los mensajes registrados por [[Yii::error()]].
* `warning`: correspondiente a los mensajes registrados por [[Yii::warning()]].
* `info`: correspondiente a los mensajes registrados por [[Yii::info()]].
-* `trace`: correspondiente a los mensajes registrados por [[Yii::trace()]].
+* `trace`: correspondiente a los mensajes registrados por [[Yii::debug()]].
* `profile`: correspondiente a los mensajes registrados por [[Yii::beginProfile()]] y [[Yii::endProfile()]], que se
explicará más detalladamente en la subsección [Perfiles](#performance-profiling).
@@ -290,7 +290,7 @@ número configurando la propiedad [[yii\log\Target::exportInterval|exportInterva
```
Debido al nivel de configuración de la liberación y exportación de mensajes, de forma predeterminada cuando se llama a
-`Yii::trace()` o cualquier otro método de registro de mensajes, NO veremos el registro de mensaje inmediatamente en
+`Yii::debug()` o cualquier otro método de registro de mensajes, NO veremos el registro de mensaje inmediatamente en
los destinos de registros. Esto podría ser un problema para algunas aplicaciones de consola de ejecución
prolongada (long-running). Para hacer que los mensajes de registro aparezcan inmediatamente en los destinos de
registro se deben establecer [[yii\log\Dispatcher::flushInterval|flushInterval]] y
diff --git a/docs/guide-es/structure-filters.md b/docs/guide-es/structure-filters.md
index 3c6cd2c..a6be6c2 100644
--- a/docs/guide-es/structure-filters.md
+++ b/docs/guide-es/structure-filters.md
@@ -94,7 +94,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("Action '{$action->uniqueId}' spent $time second.");
+ Yii::debug("Action '{$action->uniqueId}' spent $time second.");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide-fr/caching-data.md b/docs/guide-fr/caching-data.md
index d3cd58d..6d23274 100644
--- a/docs/guide-fr/caching-data.md
+++ b/docs/guide-fr/caching-data.md
@@ -100,8 +100,8 @@ Yii prend en charge un large panel de supports de stockage pour cache. Ce qui su
(une version de redis égale ou supérieure à 2.6.12 est nécessaire).
* [[yii\caching\WinCache]]: utilise le [WinCache](http://iis.net/downloads/microsoft/wincache-extension) PHP
([voir aussi l'extension](http://php.net/manual/en/book.wincache.php)).
-* [[yii\caching\XCache]]: utilise l'extension PHP [XCache](http://xcache.lighttpd.net/).
-* [[yii\caching\ZendDataCache]]: utilise le
+* [[yii\caching\XCache]] _(deprecated)_: utilise l'extension PHP [XCache](http://xcache.lighttpd.net/).
+* [[yii\caching\ZendDataCache]] _(deprecated)_: utilise le
[cache de données Zend](http://files.zend.com/help/Zend-Server-6/zend-server.htm#data_cache_component.htm)
en tant que médium de cache sous-jacent.
diff --git a/docs/guide-fr/concept-events.md b/docs/guide-fr/concept-events.md
index 46a6196..000e150 100644
--- a/docs/guide-fr/concept-events.md
+++ b/docs/guide-fr/concept-events.md
@@ -263,7 +263,7 @@ Pour gérer l'évenement `EVENT_DANCE` déclenché par n'importe laquelle de ces
```php
Event::on(DanceEventInterface::class, DanceEventInterface::EVENT_DANCE, function ($event) {
- Yii::trace(get_class($event->sender) . ' danse'); // enregistrer le message disant que le chien ou le développeur danse.
+ Yii::debug(get_class($event->sender) . ' danse'); // enregistrer le message disant que le chien ou le développeur danse.
})
```
diff --git a/docs/guide-fr/db-migrations.md b/docs/guide-fr/db-migrations.md
index e44a4ac..6498cdf 100644
--- a/docs/guide-fr/db-migrations.md
+++ b/docs/guide-fr/db-migrations.md
@@ -169,7 +169,7 @@ génère
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -179,7 +179,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -203,7 +203,7 @@ génère
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -215,7 +215,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -240,7 +240,7 @@ génère
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -252,7 +252,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -284,7 +284,7 @@ génère
class m160328_040430_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -332,7 +332,7 @@ class m160328_040430_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -477,7 +477,7 @@ génère
class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -524,7 +524,7 @@ class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migra
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
diff --git a/docs/guide-fr/output-data-providers.md b/docs/guide-fr/output-data-providers.md
index 4f8b6c2..392ca2c 100644
--- a/docs/guide-fr/output-data-providers.md
+++ b/docs/guide-fr/output-data-providers.md
@@ -220,7 +220,7 @@ class CsvDataProvider extends BaseDataProvider
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -231,7 +231,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareModels()
{
@@ -260,7 +260,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareKeys($models)
{
@@ -282,7 +282,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareTotalCount()
{
diff --git a/docs/guide-fr/output-data-widgets.md b/docs/guide-fr/output-data-widgets.md
index b5fa958..dbe0ff1 100644
--- a/docs/guide-fr/output-data-widgets.md
+++ b/docs/guide-fr/output-data-widgets.md
@@ -561,7 +561,7 @@ class UserView extends ActiveRecord
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function tableName()
{
@@ -574,7 +574,7 @@ class UserView extends ActiveRecord
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -584,7 +584,7 @@ class UserView extends ActiveRecord
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
diff --git a/docs/guide-fr/runtime-logging.md b/docs/guide-fr/runtime-logging.md
index bb0896a..8cd79ce 100644
--- a/docs/guide-fr/runtime-logging.md
+++ b/docs/guide-fr/runtime-logging.md
@@ -16,7 +16,7 @@ Dans cette section, nous décrivons principalement les deux premières étapes.
Enregistrer des messages est aussi simple que d'appeler une des méthodes suivantes :
-* [[Yii::trace()]]: enregistre un message pour garder une trace de comment un morceau de code fonctionne. Cela est utilisé principalement en développement.
+* [[Yii::debug()]]: enregistre un message pour garder une trace de comment un morceau de code fonctionne. Cela est utilisé principalement en développement.
* [[Yii::info()]]: enregistre un message qui contient quelques informations utiles.
* [[Yii::warning()]]: enregistre un message d'avertissement qui indique que quelque chose d'inattendu s'est produit.
* [[Yii::error()]]: enregistre une erreur fatale qui doit être analysée dès que possible.
@@ -24,7 +24,7 @@ Enregistrer des messages est aussi simple que d'appeler une des méthodes suivan
Ces méthodes enregistrent les messages à différents niveaux de sévérité et dans différentes catégories. Elles partagent la même signature `function ($message, $category = 'application')`, où `$message` représente le message à enregistrer, tandis que `$category` est la catégorie de ce message. Le code de l'exemple qui suit enregistre un message de trace dans la catégorie `application`:
```php
-Yii::trace('start calculating average revenue');
+Yii::debug('start calculating average revenue');
```
> Info: les messages enregistrés peuvent être des chaînes de caractères aussi bien que des données complexes telles que des tableaux ou des objets. Il est de la responsabilité des [cibles d'enregistrement](#log-targets) de traiter correctement ces messages. Par défaut, si un message enregistré n'est pas un chaîne de caractères, il est exporté comme une chaîne de caractères en appelant la méthode [[yii\helpers\VarDumper::export()]].
@@ -32,7 +32,7 @@ Yii::trace('start calculating average revenue');
Pour mieux organiser et filtrer les messages enregistrés, il est recommandé que vous spécifiiez une catégorie appropriée pour chacun des messages. Vous pouvez choisir une schéma de nommage hiérarchisé pour les catégories, ce qui facilitera le filtrage des messages par les [cibles d'enregistrement](#log-targets) sur la base de ces catégories. Un schéma de nommage simple et efficace est d'utiliser la constante magique `__METHOD__` de PHP dans les noms de catégorie. Par exemple :
```php
-Yii::trace('start calculating average revenue', __METHOD__);
+Yii::debug('start calculating average revenue', __METHOD__);
```
La constante magique `__METHOD__` est évaluée comme le nom de la méthode (préfixée par le nom pleinement qualifié de la classe), là où la constante apparaît. Par exemple, elle est égale à `'app\controllers\RevenueController::calculate'` si la ligne suivante est utilisée dans cette méthode.
@@ -100,7 +100,7 @@ La propriété [[yii\log\Target::levels|levels]] accepte un tableau constitué d
* `error`: correspondant aux messages enregistrés par [[Yii::error()]].
* `warning`: correspondant aux messages enregistrés par [[Yii::warning()]].
* `info`: correspondant aux messages enregistrés par [[Yii::info()]].
-* `trace`: correspondant aux messages enregistrés par [[Yii::trace()]].
+* `trace`: correspondant aux messages enregistrés par [[Yii::debug()]].
* `profile`: correspondant aux messages enregistrés par [[Yii::beginProfile()]] et [[Yii::endProfile()]], et qui sera expliqué en détails dans la sous-section [Profilage de la performance](#performance-profiling).
Si vous ne spécifiez pas la propriété [[yii\log\Target::levels|levels]], cela signifie que la cible traitera les messages de *n'importe quel* niveau de sévérité.
@@ -222,7 +222,7 @@ Lorsque l'[[yii\log\Logger|objet *logger*]] purge les messages enregistrés vers
]
```
-À cause des niveaux de purge et d'exportation, par défaut, lorsque vous appelez `Yii::trace()` ou toute autre méthode d'enregistrement, vous ne voyez PAS immédiatement le message enregistré dans la cible. Cela peut représenter un problème pour pour certaines applications de console qui durent longtemps. Pour faire en sorte que les messages apparaissent immédiatement dans les cibles d'enregistrement, vous devriez définir les propriétés [[yii\log\Dispatcher::flushInterval|flushInterval]] et [[yii\log\Target::exportInterval|exportInterval]] toutes deux à 1, comme montré ci-après :
+À cause des niveaux de purge et d'exportation, par défaut, lorsque vous appelez `Yii::debug()` ou toute autre méthode d'enregistrement, vous ne voyez PAS immédiatement le message enregistré dans la cible. Cela peut représenter un problème pour pour certaines applications de console qui durent longtemps. Pour faire en sorte que les messages apparaissent immédiatement dans les cibles d'enregistrement, vous devriez définir les propriétés [[yii\log\Dispatcher::flushInterval|flushInterval]] et [[yii\log\Target::exportInterval|exportInterval]] toutes deux à 1, comme montré ci-après :
```php
return [
diff --git a/docs/guide-fr/structure-filters.md b/docs/guide-fr/structure-filters.md
index 4fda236..9a7a6ca 100644
--- a/docs/guide-fr/structure-filters.md
+++ b/docs/guide-fr/structure-filters.md
@@ -72,7 +72,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("Action '{$action->uniqueId}' spent $time second.");
+ Yii::debug("Action '{$action->uniqueId}' spent $time second.");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide-ja/concept-events.md b/docs/guide-ja/concept-events.md
index 7200c9b..96eddfb 100644
--- a/docs/guide-ja/concept-events.md
+++ b/docs/guide-ja/concept-events.md
@@ -209,7 +209,7 @@ use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' が挿入されました');
+ Yii::debug(get_class($event->sender) . ' が挿入されました');
});
```
@@ -292,7 +292,7 @@ class Developer extends Component implements DanceEventInterface
```php
Event::on(DanceEventInterface::class, DanceEventInterface::EVENT_DANCE, function ($event) {
- Yii::trace(get_class($event->sender) . ' が躍り上がって喜んだ。'); // 犬または開発者が躍り上がって喜んだことをログに記録。
+ Yii::debug(get_class($event->sender) . ' が躍り上がって喜んだ。'); // 犬または開発者が躍り上がって喜んだことをログに記録。
})
```
diff --git a/docs/guide-ja/db-migrations.md b/docs/guide-ja/db-migrations.md
index a347f0e..c70dd6a 100644
--- a/docs/guide-ja/db-migrations.md
+++ b/docs/guide-ja/db-migrations.md
@@ -196,7 +196,7 @@ yii migrate/create create_post_table
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -206,7 +206,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -230,7 +230,7 @@ yii migrate/create create_post_table --fields="title:string,body:text"
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -242,7 +242,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -267,7 +267,7 @@ yii migrate/create create_post_table --fields="title:string(12):notNull:unique,b
class m150811_220037_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -279,7 +279,7 @@ class m150811_220037_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -313,7 +313,7 @@ yii migrate/create create_post_table --fields="author_id:integer:notNull:foreign
class m160328_040430_create_post_table extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -361,7 +361,7 @@ class m160328_040430_create_post_table extends Migration
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
@@ -521,7 +521,7 @@ yii migrate/create create_junction_table_for_post_and_tag_tables --fields="creat
class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migration
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function up()
{
@@ -568,7 +568,7 @@ class m160328_041642_create_junction_table_for_post_and_tag_tables extends Migra
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function down()
{
diff --git a/docs/guide-ja/output-data-providers.md b/docs/guide-ja/output-data-providers.md
index 18402dd..60032ac 100644
--- a/docs/guide-ja/output-data-providers.md
+++ b/docs/guide-ja/output-data-providers.md
@@ -250,7 +250,7 @@ class CsvDataProvider extends BaseDataProvider
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -261,7 +261,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareModels()
{
@@ -290,7 +290,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareKeys($models)
{
@@ -312,7 +312,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareTotalCount()
{
diff --git a/docs/guide-ja/output-data-widgets.md b/docs/guide-ja/output-data-widgets.md
index b5367ea..fb296af 100644
--- a/docs/guide-ja/output-data-widgets.md
+++ b/docs/guide-ja/output-data-widgets.md
@@ -637,7 +637,7 @@ class UserView extends ActiveRecord
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function tableName()
{
@@ -650,7 +650,7 @@ class UserView extends ActiveRecord
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -660,7 +660,7 @@ class UserView extends ActiveRecord
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
diff --git a/docs/guide-ja/runtime-logging.md b/docs/guide-ja/runtime-logging.md
index f644b63..5a4f308 100644
--- a/docs/guide-ja/runtime-logging.md
+++ b/docs/guide-ja/runtime-logging.md
@@ -17,7 +17,7 @@ Yii のロギングフレームワークを使うためには、下記のステ
ログメッセージを記録することは、次のログ記録メソッドのどれかを呼び出すだけの簡単なことです。
-* [[Yii::trace()]]: コードの断片がどのように走ったかをトレースするメッセージを記録します。主として開発のために使用します。
+* [[Yii::debug()]]: コードの断片がどのように走ったかをトレースするメッセージを記録します。主として開発のために使用します。
* [[Yii::info()]]: 何らかの有用な情報を伝えるメッセージを記録します。
* [[Yii::warning()]]: 何か予期しないことが発生したことを示す警告メッセージを記録します。
* [[Yii::error()]]: 出来るだけ早急に調査すべき致命的なエラーを記録します。
@@ -27,7 +27,7 @@ Yii のロギングフレームワークを使うためには、下記のステ
次のコードサンプルは、トレースメッセージをデフォルトのカテゴリである `application` の下に記録するものです。
```php
-Yii::trace('平均収益の計算を開始');
+Yii::debug('平均収益の計算を開始');
```
> Info: ログメッセージは文字列でも、配列やオブジェクトのような複雑なデータでも構いません。
@@ -40,7 +40,7 @@ Yii::trace('平均収益の計算を開始');
これは、Yii フレームワークのコアコードでも使われている方法です。例えば、
```php
-Yii::trace('平均収益の計算を開始', __METHOD__);
+Yii::debug('平均収益の計算を開始', __METHOD__);
```
`__METHOD__` という定数は、それが出現する場所のメソッド名 (完全修飾のクラス名が前置されます) として評価されます。
@@ -116,7 +116,7 @@ Yii は下記のログターゲットをあらかじめ内蔵しています。
* `error`: [[Yii::error()]] によって記録されたメッセージに対応。
* `warning`: [[Yii::warning()]] によって記録されたメッセージに対応。
* `info`: [[Yii::info()]] によって記録されたメッセージに対応。
-* `trace`: [[Yii::trace()]] によって記録されたメッセージに対応。
+* `trace`: [[Yii::debug()]] によって記録されたメッセージに対応。
* `profile`: [[Yii::beginProfile()]] と [[Yii::endProfile()]] によって記録されたメッセージに対応。
これについては、[プロファイリング](#performance-profiling) の項で詳細に説明します。
@@ -255,7 +255,7 @@ return [
]
```
-デフォルトの状態では、吐き出しとエクスポートの間隔の設定のために、`Yii::trace()` やその他のログ記録メソッドを呼んでも、ただちには、ログメッセージはログターゲットに出現しません。
+デフォルトの状態では、吐き出しとエクスポートの間隔の設定のために、`Yii::debug()` やその他のログ記録メソッドを呼んでも、ただちには、ログメッセージはログターゲットに出現しません。
このことは、長時間にわたって走るコンソールアプリケーションでは、問題になる場合もあります。
各ログメッセージがただちにログターゲットに出現するようにするためには、下記のように、[[yii\log\Dispatcher::flushInterval|flushInterval]] と [[yii\log\Target::exportInterval|exportInterval]] の両方を 1 に設定しなければなりません。
diff --git a/docs/guide-ja/structure-filters.md b/docs/guide-ja/structure-filters.md
index f91e7bc..9bc9cf8 100644
--- a/docs/guide-ja/structure-filters.md
+++ b/docs/guide-ja/structure-filters.md
@@ -83,7 +83,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("アクション '{$action->uniqueId}' は $time 秒を消費。");
+ Yii::debug("アクション '{$action->uniqueId}' は $time 秒を消費。");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide-pt-BR/concept-events.md b/docs/guide-pt-BR/concept-events.md
index c89d482..77719ab 100644
--- a/docs/guide-pt-BR/concept-events.md
+++ b/docs/guide-pt-BR/concept-events.md
@@ -192,7 +192,7 @@ use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' is inserted');
+ Yii::debug(get_class($event->sender) . ' is inserted');
});
```
diff --git a/docs/guide-pt-BR/output-data-providers.md b/docs/guide-pt-BR/output-data-providers.md
index 872a253..c49626b 100644
--- a/docs/guide-pt-BR/output-data-providers.md
+++ b/docs/guide-pt-BR/output-data-providers.md
@@ -228,7 +228,7 @@ class CsvDataProvider extends BaseDataProvider
protected $fileObject; // SplFileObject é muito conveniente para procurar uma linha específica em um arquivo
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -239,7 +239,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareModels()
{
@@ -265,7 +265,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareKeys($models)
{
@@ -285,7 +285,7 @@ class CsvDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareTotalCount()
{
diff --git a/docs/guide-pt-BR/runtime-logging.md b/docs/guide-pt-BR/runtime-logging.md
index d487244..555df02 100644
--- a/docs/guide-pt-BR/runtime-logging.md
+++ b/docs/guide-pt-BR/runtime-logging.md
@@ -16,7 +16,7 @@ Nesta seção, vamos descrever principalmente os dois primeiros passos.
Gravar mensagens de log é tão simples como chamar um dos seguintes métodos de registro:
-* [[Yii::trace()]]: gravar uma mensagem para rastrear como um determinado trecho de código é executado. Isso é principalmente para o uso de desenvolvimento.
+* [[Yii::debug()]]: gravar uma mensagem para rastrear como um determinado trecho de código é executado. Isso é principalmente para o uso de desenvolvimento.
* [[Yii::info()]]: gravar uma mensagem que transmite algumas informações úteis.
* [[Yii::warning()]]: gravar uma mensagem de aviso que indica que algo inesperado aconteceu.
* [[Yii::error()]]: gravar um erro fatal que deve ser investigado o mais rápido possível.
@@ -24,7 +24,7 @@ Gravar mensagens de log é tão simples como chamar um dos seguintes métodos de
Estes métodos gravam mensagens de log em vários *níveis* e *categorias*. Eles compartilham a mesma assinatura de função `function ($message, $category = 'application')`, onde `$message` significa a mensagem de log a ser gravada, enquanto `$category` é a categoria da mensagem de log. O código no exemplo a seguir registra uma mensagem de rastreamento sob a categoria padrão `application`:
```php
-Yii::trace('start calculating average revenue');
+Yii::debug('start calculating average revenue');
```
> Observação: Mensagens de log podem ser strings, bem como dados complexos, tais como arrays ou objetos. É da responsabilidade dos [destinos de log](#log-targets) lidar adequadamente com as mensagens de log. Por padrão, se uma mensagem de log não for uma string, ela será exportada como uma string chamando [[yii\helpers\VarDumper::export()]].
@@ -32,7 +32,7 @@ Yii::trace('start calculating average revenue');
Para melhor organizar e filtrar as mensagens de log, é recomendável que você especifique uma categoria apropriada para cada mensagem de log. Você pode escolher um esquema de nomenclatura hierárquica para as categorias, o que tornará mais fácil para os [destinos de log](#log-targets) filtrar mensagens com base em suas categorias. Um esquema de nomes simples, mas eficaz é usar a constante mágica PHP `__METHOD__` para os nomes das categorias. Esta é também a abordagem utilizada no código central do framework Yii. Por exemplo,
```php
-Yii::trace('start calculating average revenue', __METHOD__);
+Yii::debug('start calculating average revenue', __METHOD__);
```
A constante `__METHOD__` corresponde ao nome do método (prefixado com o caminho completo do nome da classe) onde a constante aparece. Por exemplo, é igual a string `'app\controllers\RevenueController::calculate'` se o código acima for chamado dentro deste método.
@@ -100,7 +100,7 @@ A propriedade [[yii\log\Target::levels|levels]] é um array que consiste em um o
* `error`: corresponde a mensagens logadas por [[Yii::error()]].
* `warning`: corresponde a mensagens logadas por [[Yii::warning()]].
* `info`: corresponde a mensagens logadas por [[Yii::info()]].
-* `trace`: corresponde a mensagens logadas por [[Yii::trace()]].
+* `trace`: corresponde a mensagens logadas por [[Yii::debug()]].
* `profile`: corresponde a mensagens logadas por [[Yii::beginProfile()]] e [[Yii::endProfile()]], que será explicado em mais detalhes na subseção [Perfil de Desempenho](#performance-profiling).
Se você não especificar a propriedade [[yii\log\Target::levels|levels]], significa que o alvo de log processará mensagens de *qualquer* nível.
@@ -218,7 +218,7 @@ Quando o [[yii\log\Logger|logger object]] libera mensagens de log para os [alvos
]
```
-Devido a configuração de nível, liberação e exportação, por padrão quando você chama `Yii::trace()` ou qualquer outro método de log, você NÃO verá a mensagem de log imediatamente no destino. Isto poderia ser um problema para algumas aplicações console de longa execução. Para fazer cada mensagem de log aparecer imediatamente no destino, você deve configurar ambos [[yii\log\Dispatcher::flushInterval|flushInterval]] e [[yii\log\Target::exportInterval|exportInterval]] para 1, como mostrado a seguir:
+Devido a configuração de nível, liberação e exportação, por padrão quando você chama `Yii::debug()` ou qualquer outro método de log, você NÃO verá a mensagem de log imediatamente no destino. Isto poderia ser um problema para algumas aplicações console de longa execução. Para fazer cada mensagem de log aparecer imediatamente no destino, você deve configurar ambos [[yii\log\Dispatcher::flushInterval|flushInterval]] e [[yii\log\Target::exportInterval|exportInterval]] para 1, como mostrado a seguir:
```php
return [
diff --git a/docs/guide-pt-BR/structure-filters.md b/docs/guide-pt-BR/structure-filters.md
index 4048afc..2aecde8 100644
--- a/docs/guide-pt-BR/structure-filters.md
+++ b/docs/guide-pt-BR/structure-filters.md
@@ -105,7 +105,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("Action '{$action->uniqueId}' spent $time second.");
+ Yii::debug("Action '{$action->uniqueId}' spent $time second.");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide-ru/caching-data.md b/docs/guide-ru/caching-data.md
index 25072e6..ec7bf2b 100644
--- a/docs/guide-ru/caching-data.md
+++ b/docs/guide-ru/caching-data.md
@@ -252,7 +252,7 @@ $result = Customer::getDb()->cache(function ($db) {
### Очистка кэша
-Для очистки всего кэша, вы можете вызвать [[yii\caching\Cache::clear()]].
+Для очистки всего кэша, вы можете вызвать [[yii\caching\Cache::flush()]].
Также вы можете очистить кэш из консоли, вызвав `yii cache/clear`.
- `yii cache`: отображает список доступных кэширующих компонентов приложения
diff --git a/docs/guide-ru/concept-events.md b/docs/guide-ru/concept-events.md
index d3ec8a5..bd01fd2 100644
--- a/docs/guide-ru/concept-events.md
+++ b/docs/guide-ru/concept-events.md
@@ -188,7 +188,7 @@ use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' добавлен');
+ Yii::debug(get_class($event->sender) . ' добавлен');
});
```
@@ -266,7 +266,7 @@ class Developer extends Component implements DanceEventInterface
```php
Event::on(DanceEventInterface::class, DanceEventInterface::EVENT_DANCE, function ($event) {
- Yii::trace(get_class($event->sender) . ' just danced'); // Оставит запись в журнале о том, что кто-то танцевал
+ Yii::debug(get_class($event->sender) . ' just danced'); // Оставит запись в журнале о том, что кто-то танцевал
});
```
diff --git a/docs/guide-ru/input-validation.md b/docs/guide-ru/input-validation.md
index dacd5f9..6e3cca6 100644
--- a/docs/guide-ru/input-validation.md
+++ b/docs/guide-ru/input-validation.md
@@ -358,8 +358,8 @@ class MyForm extends Model
public function validateCountry($attribute, $params)
{
- if (!in_array($this->$attribute, ['USA', 'Web'])) {
- $this->addError($attribute, 'Страна должна быть либо "USA" или "Web".');
+ if (!in_array($this->$attribute, ['USA', 'Indonesia'])) {
+ $this->addError($attribute, 'Страна должна быть либо "USA" или "Indonesia".');
}
}
}
@@ -384,7 +384,9 @@ class MyForm extends Model
Вы можете реализовать свою логику проверки путем переопределения метода
[[yii\validators\Validator::validateAttribute()]]. Если атрибут не прошел проверку, вызвать
[[yii\base\Model::addError()]],
-чтобы сохранить сообщение об ошибке в модели, как это делают [встроенные валидаторы](#inline-validators). Например:
+чтобы сохранить сообщение об ошибке в модели, как это делают [встроенные валидаторы](#inline-validators).
+
+Валидация может быть помещена в отдельный класс [[components/validators/CountryValidator]]. В этом случае можно использовать метод [[yii\validators\Validator::addError()]] для того, чтобы добавить своё сообщение об ошибке в модель:
```php
namespace app\components;
@@ -395,8 +397,8 @@ class CountryValidator extends Validator
{
public function validateAttribute($model, $attribute)
{
- if (!in_array($model->$attribute, ['USA', 'Web'])) {
- $this->addError($model, $attribute, 'Страна должна быть либо "USA" или "Web".');
+ if (!in_array($model->$attribute, ['USA', 'Indonesia'])) {
+ $this->addError($model, $attribute, 'Страна должна быть либо "{country1}" либо "{country2}".', ['country1' => 'USA', 'country2' => 'Indonesia']);
}
}
}
diff --git a/docs/guide-ru/runtime-logging.md b/docs/guide-ru/runtime-logging.md
index cf7bcb1..84390da 100644
--- a/docs/guide-ru/runtime-logging.md
+++ b/docs/guide-ru/runtime-logging.md
@@ -14,7 +14,7 @@ Yii предоставляет мощную, гибко настраиваему
Запись сообщений лога осуществляется вызовом одного из следующих методов:
-* [[Yii::trace()]]: записывает сообщения для отслеживания выполнения кода приложения. Используется, в основном, при разработке.
+* [[Yii::debug()]]: записывает сообщения для отслеживания выполнения кода приложения. Используется, в основном, при разработке.
* [[Yii::info()]]: записывает сообщение, содержащее какую-либо полезную информацию.
* [[Yii::warning()]]: записывает *тревожное* сообщение при возникновении неожиданного события.
* [[Yii::error()]]: записывает критическую ошибку, на которую нужно, как можно скорее, обратить внимаение.
@@ -22,7 +22,7 @@ Yii предоставляет мощную, гибко настраиваему
Эти методы позволяют записывать сообщения разных *уровней важности* и *категорий*. Они имеют одинаковое описание функции `function ($message, $category = 'application')`, где `$message` передает сообщение для записи, а `$category` - категорию сообщения. В следующем примере будет записано *trace* сообщение с категорией по умолчанию `application`:
```php
-Yii::trace('start calculating average revenue');
+Yii::debug('start calculating average revenue');
```
> Note: Сообщение может быть как строкой так и объектом или массивом. За корректную работу с содержимым сообщения отвечают [цели лога](#log-targets). По умолчанию, если сообщение не является строкой, оно будет приведено к строковому типу при помощи [[yii\helpers\VarDumper::export()]].
@@ -30,7 +30,7 @@ Yii::trace('start calculating average revenue');
Для упрощения работы с сообщениями лога и их фильтрации, рекомендуется явно указывать подходящую категорию для каждого сообщения. Возможно использование иерархической системы именования категорий, что значительно упростит [целям лога](#log-targets) фильтрацию сообщений по категориям. Простым и эффективным способом именования категорий является использование магической PHP константы `__METHOD__`. Такой подход используется в ядре фреймворка Yii. Например,
```php
-Yii::trace('начало вычисления среднего дохода', __METHOD__);
+Yii::debug('начало вычисления среднего дохода', __METHOD__);
```
Константа `__METHOD__` вычисляется как имя метода (включая полное имя класса), в котором она использована. Например, её значение будет вычислено как `'app\controllers\RevenueController::calculate'`, если показанный выше код вызывается в соответствующем методе.
@@ -101,7 +101,7 @@ return [
* `error`: соответствует сообщениям, сохраненным методом [[Yii::error()]].
* `warning`: соответствует сообщениям, сохраненным методом [[Yii::warning()]].
* `info`: соответствует сообщениям, сохраненным методом [[Yii::info()]].
-* `trace`: соответствует сообщениям, сохраненным методом [[Yii::trace()]].
+* `trace`: соответствует сообщениям, сохраненным методом [[Yii::debug()]].
* `profile`: соответствует сообщениям, сохраненным методами [[Yii::beginProfile()]] и [[Yii::endProfile()]], подробнее о которых написано в подразделе [Профилирование производительности](#performance-profiling).
Если свойство [[yii\log\Target::levels|levels]] не задано, цель логов будет обрабатывать сообщения с *любым* уровнем важности.
@@ -222,7 +222,7 @@ return [
]
```
-Из-за того, что значения максимального количества сообщений для передачи и выгрузки по умолчанию достаточно велико, при вызове метода `Yii::trace()`, или любого другого метода логгирования, сообщение не появится сразу в файле или таблице базы данных. Такое поведение может стать проблемой, например, в консольных приложениях с большим временем исполнения. Для того, чтобы все сообщения логов сразу же попадали в лог, необходимо установить значения свойств [[yii\log\Dispatcher::flushInterval|flushInterval]] и [[yii\log\Target::exportInterval|exportInterval]] равными 1, например так:
+Из-за того, что значения максимального количества сообщений для передачи и выгрузки по умолчанию достаточно велико, при вызове метода `Yii::debug()`, или любого другого метода логгирования, сообщение не появится сразу в файле или таблице базы данных. Такое поведение может стать проблемой, например, в консольных приложениях с большим временем исполнения. Для того, чтобы все сообщения логов сразу же попадали в лог, необходимо установить значения свойств [[yii\log\Dispatcher::flushInterval|flushInterval]] и [[yii\log\Target::exportInterval|exportInterval]] равными 1, например так:
```php
return [
diff --git a/docs/guide-ru/structure-filters.md b/docs/guide-ru/structure-filters.md
index 7dabb95..df4af7f 100644
--- a/docs/guide-ru/structure-filters.md
+++ b/docs/guide-ru/structure-filters.md
@@ -87,7 +87,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("Action '{$action->uniqueId}' spent $time second.");
+ Yii::debug("Action '{$action->uniqueId}' spent $time second.");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide-zh-CN/concept-events.md b/docs/guide-zh-CN/concept-events.md
index d00392e..e8782a6 100644
--- a/docs/guide-zh-CN/concept-events.md
+++ b/docs/guide-zh-CN/concept-events.md
@@ -182,7 +182,7 @@ use yii\base\Event;
use yii\db\ActiveRecord;
Event::on(ActiveRecord::class, ActiveRecord::EVENT_AFTER_INSERT, function ($event) {
- Yii::trace(get_class($event->sender) . ' is inserted');
+ Yii::debug(get_class($event->sender) . ' is inserted');
});
```
diff --git a/docs/guide-zh-CN/db-active-record.md b/docs/guide-zh-CN/db-active-record.md
index a3d85aa..fca3c99 100644
--- a/docs/guide-zh-CN/db-active-record.md
+++ b/docs/guide-zh-CN/db-active-record.md
@@ -796,7 +796,7 @@ use yii\db\ActiveRecord;
class Comment extends ActiveRecord
{
/**
- * @inheritdoc
+ * {@inheritdoc}
* @return CommentQuery
*/
public static function find()
diff --git a/docs/guide-zh-CN/structure-filters.md b/docs/guide-zh-CN/structure-filters.md
index 68f8ae6..0c83922 100644
--- a/docs/guide-zh-CN/structure-filters.md
+++ b/docs/guide-zh-CN/structure-filters.md
@@ -84,7 +84,7 @@ class ActionTimeFilter extends ActionFilter
public function afterAction($action, $result)
{
$time = microtime(true) - $this->_startTime;
- Yii::trace("Action '{$action->uniqueId}' spent $time second.");
+ Yii::debug("Action '{$action->uniqueId}' spent $time second.");
return parent::afterAction($action, $result);
}
}
diff --git a/docs/guide/caching-data.md b/docs/guide/caching-data.md
index 54d789d..45b123f 100644
--- a/docs/guide/caching-data.md
+++ b/docs/guide/caching-data.md
@@ -319,6 +319,13 @@ $result = Customer::getDb()->cache(function ($db) {
The query caching described above has the advantage that you may specify flexible cache dependencies
and are potentially more efficient.
+Since 2.0.14 you can use the following shortcuts:
+
+```php
+(new Query())->cache(7200)->all();
+// and
+User::find()->cache(7200)->all();
+```
### Clearing Cache
diff --git a/docs/guide/caching-fragment.md b/docs/guide/caching-fragment.md
index a9fef47..5a5e9a1 100644
--- a/docs/guide/caching-fragment.md
+++ b/docs/guide/caching-fragment.md
@@ -174,3 +174,6 @@ if ($this->beginCache($id1)) {
The [[yii\base\View::renderDynamic()|renderDynamic()]] method takes a piece of PHP code as its parameter.
The return value of the PHP code is treated as the dynamic content. The same PHP code will be executed
for every request, no matter the enclosing fragment is being served from cached or not.
+
+> Note: since version 2.0.14 a dynamic content API is exposed via the [[yii\base\DynamicContentAwareInterface]] interface and its [[yii\base\DynamicContentAwareTrait]] trait.
+ As an example, you may refer to the [[yii\widgets\FragmentCache]] class.
diff --git a/docs/guide/concept-events.md b/docs/guide/concept-events.md
index ba79137..61cec01 100644
--- a/docs/guide/concept-events.md
+++ b/docs/guide/concept-events.md
@@ -370,7 +370,7 @@ $foo = new Foo();
$foo->on('foo.event.*', function ($event) {
// triggered for any event, which name starts on 'foo.event.'
- Yii::trace('trigger event: ' . $event->name);
+ Yii::debug('trigger event: ' . $event->name);
});
```
@@ -382,7 +382,7 @@ use Yii;
Event::on('app\models\*', 'before*', function ($event) {
// triggered for any class in namespace 'app\models' for any event, which name starts on 'before'
- Yii::trace('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender));
+ Yii::debug('trigger event: ' . $event->name . ' for class: ' . get_class($event->sender));
});
```
@@ -394,7 +394,7 @@ use Yii;
Event::on('*', '*', function ($event) {
// triggered for any event at any class
- Yii::trace('trigger event: ' . $event->name);
+ Yii::debug('trigger event: ' . $event->name);
});
```
diff --git a/docs/guide/db-active-record.md b/docs/guide/db-active-record.md
index 8bb765f..a7938fc 100644
--- a/docs/guide/db-active-record.md
+++ b/docs/guide/db-active-record.md
@@ -472,7 +472,7 @@ $customer->loadDefaultValues();
### Attributes Typecasting
-Being populated by query results [[yii\db\ActiveRecord]] performs automatic typecast for its attribute values, using
+Being populated by query results, [[yii\db\ActiveRecord]] performs automatic typecast for its attribute values, using
information from [database table schema](db-dao.md#database-schema). This allows data retrieved from table column
declared as integer to be populated in ActiveRecord instance with PHP integer, boolean with boolean and so on.
However, typecasting mechanism has several limitations:
@@ -490,7 +490,33 @@ converted during saving process.
> Tip: you may use [[yii\behaviors\AttributeTypecastBehavior]] to facilitate attribute values typecasting
on ActiveRecord validation or saving.
+
+Since 2.0.14, Yii ActiveRecord supports complex data types, such as JSON or multidimensional arrays.
+
+#### JSON in MySQL and PostgreSQL
+
+After data population, the value from JSON column will be automatically decoded from JSON according to standard JSON
+decoding rules.
+
+To save attribute value to a JSON column, ActiveRecord will automatically create a [[yii\db\JsonExpression|JsonExpression]] object
+that will be encoded to a JSON string on [QueryBuilder](db-query-builder.md) level.
+
+#### Arrays in PostgreSQL
+
+After data population, the value from Array column will be automatically decoded from PgSQL notation to an [[yii\db\ArrayExpression|ArrayExpression]]
+object. It implements PHP `ArrayAccess` interface, so you can use it as an array, or call `->getValue()` to get the array itself.
+
+To save attribute value to an array column, ActiveRecord will automatically create an [[yii\db\ArrayExpression|ArrayExpression]] object
+that will be encoded by [QueryBuilder](db-query-builder.md) to an PgSQL string representation of array.
+
+You can also use conditions for JSON columns:
+
+```php
+$query->andWhere(['=', 'json', new ArrayExpression(['foo' => 'bar'])
+```
+To learn more about expressions building system read the [Query Builder – Adding custom Conditions and Expressions](db-query-builder.md#adding-custom-conditions-and-expressions)
+article.
### Updating Multiple Rows
diff --git a/docs/guide/db-dao.md b/docs/guide/db-dao.md
index c850717..e485c14 100644
--- a/docs/guide/db-dao.md
+++ b/docs/guide/db-dao.md
@@ -257,6 +257,21 @@ Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [
])->execute();
```
+Another useful method is [[yii\db\Command::upsert()|upsert()]]. Upsert is an atomic operation that inserts rows into
+a database table if they do not already exist (matching unique constraints), or update them if they do:
+
+```php
+Yii::$app->db->createCommand()->upsert('pages', [
+ 'name' => 'Front page',
+ 'url' => 'http://example.com/', // url is unique
+ 'visits' => 0,
+], [
+ 'visits' => new \yii\db\Expression('visits + 1'),
+], $params)->execute();
+```
+
+The code above will either insert a new page record or increment its visit counter atomically.
+
Note that the aforementioned methods only create the query and you always have to call [[yii\db\Command::execute()|execute()]]
to actually run them.
diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md
index b23d1e7..1086f44 100644
--- a/docs/guide/db-query-builder.md
+++ b/docs/guide/db-query-builder.md
@@ -160,12 +160,12 @@ are in the ["Quoting Tables" section of the "Database Access Objects" guide](gui
### [[yii\db\Query::where()|where()]]
The [[yii\db\Query::where()|where()]] method specifies the `WHERE` fragment of a SQL query. You can use one of
-the three formats to specify a `WHERE` condition:
+the four formats to specify a `WHERE` condition:
- string format, e.g., `'status=1'`
- hash format, e.g. `['status' => 1, 'type' => 2]`
- operator format, e.g. `['like', 'name', 'test']`
-
+- object format, e.g. `new LikeCondition('name', 'LIKE', 'test')`
#### String Format
@@ -255,6 +255,9 @@ the operator can be one of the following:
- `between`: operand 1 should be the column name, and operand 2 and 3 should be the
starting and ending values of the range that the column is in.
For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`.
+ In case you need to build a condition where value is between two columns (like `11 BETWEEN min_id AND max_id`),
+ you should use [[yii\db\conditions\BetweenColumnsCondition|BetweenColumnsCondition]].
+ See [Conditions – Object Format](#object-format) chapter to learn more about object definition of conditions.
- `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN`
in the generated condition.
@@ -306,6 +309,41 @@ the operator can be one of the following:
Using the Operator Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here
you do not have to add parameters manually.
+#### Object Format
+
+Object Form is available since 2.0.14 and is both most powerful and most complex way to define conditions.
+You need to follow it either if you want to build your own abstraction over query builder or if you want to implement
+your own complex conditions.
+
+Instances of condition classes are immutable. Their only purpose is to store condition data and provide getters
+for condition builders. Condition builder is a class that holds the logic that transforms data
+stored in condition into the SQL expression.
+
+Internally the formats described above are implicitly converted to object format prior to building raw SQL,
+so it is possible to combine formats in a single condition:
+
+```php
+$query->andWhere(new OrCondition([
+ new InCondition('type', 'in', $types),
+ ['like', 'name', '%good%'],
+ 'disabled=false'
+]))
+```
+
+Conversion from operator format into object format is performed according to
+[[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]] property, that maps operators names
+to representative class names:
+
+- `AND`, `OR` -> `yii\db\conditions\ConjunctionCondition`
+- `NOT` -> `yii\db\conditions\NotCondition`
+- `IN`, `NOT IN` -> `yii\db\conditions\InCondition`
+- `BETWEEN`, `NOT BETWEEN` -> `yii\db\conditions\BetweenCondition`
+
+And so on.
+
+Using the object format makes it possible to create your own conditions or to change the way default ones are built.
+See [Creating Custom Conditions and Expressions](#creating-custom-conditions-and-expressions) chapter to learn more.
+
#### Appending Conditions
@@ -758,3 +796,170 @@ $unbufferedDb->close();
```
> Note: unbuffered query uses less memory on the PHP-side, but can increase the load on the MySQL server. It is recommended to design your own code with your production practice for extra massive data, [for example, divide the range for integer keys, loop them with Unbuffered Queries](https://github.com/yiisoft/yii2/issues/8420#issuecomment-296109257).
+
+### Adding custom Conditions and Expressions
+
+As it was mentioned in [Conditions – Object Format](#object-format) chapter, is is possible to create custom condition
+classes. For example, let's create a condition that will check that specific columns are less than some value.
+Using the operator format, it would look like the following:
+
+```php
+[
+ 'and',
+ '>', 'posts', $minLimit,
+ '>', 'comments', $minLimit,
+ '>', 'reactions', $minLimit,
+ '>', 'subscriptions', $minLimit
+]
+```
+
+When such condition applied once, it is fine. In case it is used multiple times in a single query it can
+be optimized a lot. Let's create a custom condition object to demonstrate it.
+
+Yii has a [[yii\db\conditions\ConditionInterface|ConditionInterface]], that must be used to mark classes, that represent
+a condition. It requires `fromArrayDefinition()` method implementation, in order to make possible to create condition
+from array format. In case you don't need it, you can implement this method with exception throwing.
+
+Since we create our custom condition class, we can build API that suits our task the most.
+
+```php
+namespace app\db\conditions;
+
+class AllGreaterCondition implements \yii\db\conditions\ConditionInterface
+{
+ private $columns;
+ private $value;
+
+ /**
+ * @param string[] $columns Array of columns that must be greater, than $value
+ * @param mixed $value the value to compare each $column against.
+ */
+ public function __construct(array $columns, $value)
+ {
+ $this->columns = $columns;
+ $this->value = $value;
+ }
+
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ throw new InvalidArgumentException('Not implemented yet, but we will do it later');
+ }
+
+ public function getColumns() { return $this->columns; }
+ public function getValue() { return $this->vaule; }
+}
+```
+
+So we can create a condition object:
+
+```php
+$conditon = new AllGreaterCondition(['col1', 'col2'], 42);
+```
+
+But `QueryBuilder` still does not know, to to make an SQL condition out of this object.
+Now we need to create a builder for this condition. It must implement [[yii\db\ExpressionBuilderInterface]]
+that requires us to implement a `build()` method.
+
+```php
+namespace app\db\conditions;
+
+class AllGreaterConditionBuilder implements \yii\db\ExpressionBuilderInterface
+{
+ use \yii\db\Condition\ExpressionBuilderTrait; // Contains constructor and `queryBuilder` property.
+
+ /**
+ * @param AllGreaterCondition $condition the condition to be built
+ * @param array $params the binding parameters.
+ */
+ public function build(ConditionInterface $condition, &$params)
+ {
+ $value = $condition->getValue();
+
+ $conditions = [];
+ foreach ($condition->getColumns() as $column) {
+ $conditions[] = new SimpleCondition($column, '>', $value);
+ }
+
+ return $this->queryBuider->buildCondition(new AndCondition($conditions), $params);
+ }
+}
+```
+
+Then simple let [[yii\db\QueryBuilder|QueryBuilder]] know about our new condition – add a mapping for it to
+the `expressionBuilders` array. It could be done right from the application configuration:
+
+```php
+'db' => [
+ 'class' => 'yii\db\mysql\Connection',
+ // ...
+ 'queryBuilder' => [
+ 'expressionBuilders' => [
+ 'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder',
+ ],
+ ],
+],
+```
+
+Now we can use our condition in `where()`:
+
+```php
+$query->andWhere(new AllGreaterCondition(['posts', 'comments', 'reactions', 'subscriptions'], $minValue));
+```
+
+If we want to make it possible to create our custom condition using operator format, we should declare it in
+[[yii\db\QueryBuilder::conditionClasses|QueryBuilder::conditionClasses]]:
+
+```php
+'db' => [
+ 'class' => 'yii\db\mysql\Connection',
+ // ...
+ 'queryBuilder' => [
+ 'expressionBuilders' => [
+ 'app\db\conditions\AllGreaterCondition' => 'app\db\conditions\AllGreaterConditionBuilder',
+ ],
+ 'conditionClasses' => [
+ 'ALL>' => 'app\db\conditions\AllGreaterCondition',
+ ],
+ ],
+],
+```
+
+And create a real implementation of `AllGreaterCondition::fromArrayDefinition()` method
+in `app\db\conditions\AllGreaterCondition`:
+
+```php
+namespace app\db\conditions;
+
+class AllGreaterCondition implements \yii\db\conditions\ConditionInterface
+{
+ // ... see the implementation above
+
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ return new static($operands[0], $operands[1]);
+ }
+}
+```
+
+After that, we can create our custom condition using shorter operator format:
+
+```php
+$query->andWhere(['ALL>', ['posts', 'comments', 'reactions', 'subscriptions'], $minValue]);
+```
+
+You might notice, that there was two concepts used: Expressions and Conditions. There is a [[yii\db\ExpressionInterface]]
+that should be used to mark objects, that require an Expression Builder class, that implements
+[[yii\db\ExpressionBuilderInterface]] to be built. Also there is a [[yii\db\condition\ConditionInterface]], that extends
+[[yii\db\ExpressionInterface|ExpressionInterface]] and should be used to objects, that can be created from array definition
+as it was shown above, but require builder as well.
+
+To summarise:
+
+- Expression – is a Data Transfer Object (DTO) for a dataset, that can be somehow compiled to some SQL
+statement (an operator, string, array, JSON, etc).
+- Condition – is an Expression superset, that aggregates multiple Expressions (or scalar values) that can be compiled
+to a single SQL condition.
+
+You can create your own classes that implement [[yii\db\ExpressionInterface|ExpressionInterface]] to hide the complexity
+of transforming data to SQL statements. You will learn more about other examples of Expressions in the
+[next article](db-active-record.md);
diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md
index a82ab1b..a60b3f6 100644
--- a/docs/guide/input-validation.md
+++ b/docs/guide/input-validation.md
@@ -387,8 +387,8 @@ class MyForm extends Model
public function validateCountry($attribute, $params, $validator)
{
- if (!in_array($this->$attribute, ['USA', 'Web'])) {
- $this->addError($attribute, 'The country must be either "USA" or "Web".');
+ if (!in_array($this->$attribute, ['USA', 'Indonesia'])) {
+ $this->addError($attribute, 'The country must be either "USA" or "Indonesia".');
}
}
}
@@ -422,7 +422,8 @@ fails the validation, call [[yii\base\Model::addError()]] to save the error mess
with [inline validators](#inline-validators).
-For example the inline validator above could be moved into new [[components/validators/CountryValidator]] class.
+For example, the inline validator above could be moved into new [[components/validators/CountryValidator]] class.
+In this case we can use [[yii\validators\Validator::addError()]] to set customized message for the model.
```php
namespace app\components;
@@ -433,8 +434,8 @@ class CountryValidator extends Validator
{
public function validateAttribute($model, $attribute)
{
- if (!in_array($model->$attribute, ['USA', 'Web'])) {
- $this->addError($model, $attribute, 'The country must be either "USA" or "Web".');
+ if (!in_array($model->$attribute, ['USA', 'Indonesia'])) {
+ $this->addError($model, $attribute, 'The country must be either "{country1}" or "{country2}".', ['country1' => 'USA', 'country2' => 'Indonesia']);
}
}
}
diff --git a/docs/guide/rest-resources.md b/docs/guide/rest-resources.md
index 3141244..32629a3 100644
--- a/docs/guide/rest-resources.md
+++ b/docs/guide/rest-resources.md
@@ -38,13 +38,17 @@ in the array if an end user requests for them via the `expand` query parameter.
// returns all fields as declared in fields()
http://localhost/users
-// only returns field id and email, provided they are declared in fields()
+// only returns "id" and "email" fields, provided they are declared in fields()
http://localhost/users?fields=id,email
-// returns all fields in fields() and field profile if it is in extraFields()
+// returns all fields in fields() and field "profile" if it is in extraFields()
http://localhost/users?expand=profile
-// only returns field id, email and profile, provided they are in fields() and extraFields()
+// returns all fields in fields(), "profile" if it is in extraFields() and "author" from profile if
+// it is in extraFields() of profile model
+http://localhost/comments?expand=post.author
+
+// only returns "id" and "email" provided they are in fields() and "profile" if it is in extraFields()
http://localhost/users?fields=id,email&expand=profile
```
diff --git a/docs/guide/start-installation.md b/docs/guide/start-installation.md
index e046066..2c67ca2 100644
--- a/docs/guide/start-installation.md
+++ b/docs/guide/start-installation.md
@@ -111,6 +111,30 @@ But there are other installation options available:
you may consider installing the [Advanced Project Template](https://github.com/yiisoft/yii2-app-advanced/blob/master/docs/guide/README.md).
+Installing Assets
+-----------------
+
+Yii relies on [Bower](http://bower.io/) and/or [NPM](https://www.npmjs.org/) packages for the asset (CSS and JavaScript) libraries installation.
+It uses Composer to obtain these libraries, allowing PHP and CSS/JavaScript package versions to resolve at the same time.
+This can be achieved either by usage of [asset-packagist.org](https://asset-packagist.org) or [composer asset plugin](https://github.com/francoispluchino/composer-asset-plugin/).
+Please refer to [Assets documentation](structure-assets.md) for more details.
+
+You may want to either manage your assets via native Bower/NPM client, use CDN or avoid assets installation entirely.
+In order to prevent assets installation via Composer, add the following lines to your 'composer.json':
+
+```json
+"replace": {
+ "bower-asset/jquery": ">=1.11.0",
+ "bower-asset/inputmask": ">=3.2.0",
+ "bower-asset/punycode": ">=1.3.0",
+ "bower-asset/yii2-pjax": ">=2.0.0"
+},
+```
+
+> Note: in case of bypassing asset installation via Composer, you are responsible for the assets installation and resolving
+> version collisions. Be prepared for possible inconsistencies among asset files from different extensions.
+
+
Verifying the Installation
--------------------------
diff --git a/docs/internals/release.md b/docs/internals/release.md
index 35d092d..c5c0eb7 100644
--- a/docs/internals/release.md
+++ b/docs/internals/release.md
@@ -53,13 +53,13 @@ You may run it with `--update` to fetch tags for all repos to get the newest inf
Making a framework release includes the following commands (apps are always released together with the framework):
- ./build release framework
- ./build release app-basic
- ./build release app-advanced
+ ./build/build release framework
+ ./build/build release app-basic
+ ./build/build release app-advanced
Making an extension release includes only one command (e.g. for redis):
- ./build release redis
+ ./build/build release redis
The default release command will release a new minor version from the currently checked out branch.
To release another version than the default, you have to specify it using the `--version` option, e.g.
diff --git a/framework/BaseYii.php b/framework/BaseYii.php
index 00c740a..6325a0e 100644
--- a/framework/BaseYii.php
+++ b/framework/BaseYii.php
@@ -11,6 +11,7 @@ use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
+use yii\base\UnknownClassException;
use yii\di\Container;
use yii\di\Instance;
use yii\helpers\VarDumper;
@@ -90,7 +91,7 @@ class BaseYii
*/
public static function getVersion()
{
- return '2.0.14-dev';
+ return '2.0.15-dev';
}
/**
@@ -425,6 +426,7 @@ class BaseYii
* @param string|array $message the message to be logged. This can be a simple string or a more
* complex data structure, such as array.
* @param string $category the category of the message.
+ * @since 2.0.14
*/
public static function debug($message, $category = 'application')
{
diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index 18798d9..f1c08c7 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -43,31 +43,34 @@ Yii Framework 2 Change Log
- Chg #15481: Removed `yii\BaseYii::powered()` method (Kolyunya, samdark)
2.0.14 under development
+2.0.15 under development
+------------------------
+
+- no changes in this release.
+
+
+2.0.14 February 18, 2018
------------------------
-- Enh #13996: Added `yii\web\View::registerJsVar()` method that allows registering JavaScript variables (Eseperio, samdark)
-- Enh #9771: Assign hidden input with its own set of HTML options via `$hiddenOptions` in activeFileInput `$options` (HanafiAhmat)
-- Bug #15536: Fixed `yii\widgets\ActiveForm::init()` for call `parent::init()` (panchenkodv)
-- Enh #14806: Added $placeFooterAfterBody option for GridView (terehru)
-- Bug #14711: Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark)
-- Enh #13814: MySQL unique index names can now contain spaces (df2)
-- Bug #15300: Fixed "Cannot read property 'style' of undefined" error at the error screen (vitorarantes)
-- Bug #15540: Fixed `yii\db\ActiveRecord::with()` unable to use relation defined via attached behavior in case `asArray` is enabled (klimov-paul)
-- Enh #15426: Added abilitiy to create and drop database views (igravity, vladis84)
-- Enh #10186: Use native `hash_equals` in `yii\base\Security::compareString()` if available, throw exception if non-strings are compared (aotd1, samdark)
-- Bug #15122: Fixed `yii\db\Command::getRawSql()` to properly replace expressions (hiscaler, samdark)
-- Enh #15496: CSRF token is now regenerated on changing identity (samdark, rhertogh)
-- Enh #15417: Added `yii\validators\FileValidator::$minFiles` (vladis84)
- Bug #8983: Only truncate the original log file for rotation (matthewyang, developeruz)
+- Bug #9342: Fixed `yii\db\ActiveQueryTrait` to apply `indexBy` after relations population in order to prevent excess queries (sammousa, silverfire)
+- Bug #11401: Fixed `yii\web\DbSession` concurrency issues when writing and regenerating IDs (samdark, andreasanta, cebe)
+- Bug #13034: Fixed `normalizePath` for windows network shares that start with two backslashes (developeruz)
- Bug #14135: Fixed `yii\web\Request::getBodyParam()` crashes on object type body params (klimov-paul)
- Bug #14157: Add support for loading default value `CURRENT_TIMESTAMP` of MySQL `datetime` field (rossoneri)
- Bug #14276: Fixed I18N format with dotted parameters (developeruz)
+- Bug #14296: Fixed log targets to throw exception in case log can not be properly exported (bizley)
- Bug #14484: Fixed `yii\validators\UniqueValidator` for target classes with a default scope (laszlovl, developeruz)
- Bug #14604: Fixed `yii\validators\CompareValidator` `compareAttribute` does not work if `compareAttribute` form ID has been changed (mikk150)
+- Bug #14711 (CVE-2018-6010): Fixed `yii\web\ErrorHandler` displaying exception message in non-debug mode (samdark)
+- Bug #14811: Fixed `yii\filters\HttpCache` to work with PHP 7.2 (samdark)
+- Bug #14859: Fixed OCI DB `defaultSchema` failure when `masterConfig` is used (lovezhl456)
- Bug #14903: Fixed route with extra dashes is executed controller while it should not (developeruz)
- Bug #14916: Fixed `yii\db\Query::each()` iterator key starts from 1 instead of 0 (Vovan-VE)
-- Bug #15046: Throw an `yii\web\HeadersAlreadySentException` if headers were sent before web response (dmirogin)
- Bug #14980: Fix looping in `yii\i18n\MessageFormatter` tokenize pattern if pattern is invalid (uaoleg, developeruz)
+- Bug #15031: Fixed incorrect string type length detection for OCI DB schema (Murolike)
+- Bug #15046: Throw an `yii\web\HeadersAlreadySentException` if headers were sent before web response (dmirogin)
+- Bug #15122: Fixed `yii\db\Command::getRawSql()` to properly replace expressions (hiscaler, samdark)
- Bug #15142: Fixed array params replacing in `yii\helpers\BaseUrl::current()` (IceJOKER)
- Bug #15169: Fixed translating a string when NULL parameter is passed (developeruz)
- Bug #15194: Fixed `yii\db\QueryBuilder::insert()` to preserve passed params when building a `INSERT INTO ... SELECT` query for MSSQL, PostgreSQL and SQLite (sergeymakinen)
@@ -75,6 +78,7 @@ Yii Framework 2 Change Log
- Bug #15234: Fixed `\yii\widgets\LinkPager` removed `tag` from `disabledListItemSubTagOptions` (SDKiller)
- Bug #15249: Controllers in subdirectories were not visible in commands list (IceJOKER)
- Bug #15270: Resolved potential race conditions when writing generated php-files (kalessil)
+- Bug #15300: Fixed "Cannot read property 'style' of undefined" error at the error screen (vitorarantes)
- Bug #15301: Fixed `ArrayHelper::filter()` to work properly with `0` in values (hhniao)
- Bug #15302: Fixed `yii\caching\DbCache` so that `getValues` now behaves the same as `getValue` with regards to streams (edwards-sj)
- Bug #15317: Regenerate CSRF token if an empty value is given (sammousa)
@@ -89,39 +93,89 @@ Yii Framework 2 Change Log
- Bug #15440: Fixed `yii\behaviors\AttributeTypecastBehavior::$attributeTypes` auto-detection fails for rule, which specify attribute with '!' prefix (klimov-paul)
- Bug #15462: Fixed `accessChecker` configuration error (developeruz)
- Bug #15494: Fixed missing `WWW-Authenticate` header (developeruz)
+- Bug #15522: Fixed `yii\db\ActiveRecord::refresh()` method does not use an alias in the condition (vladis84)
+- Bug #15523: `yii\web\Session` settings could now be configured after session is started (StalkAlex, rob006, daniel1302, samdark)
+- Bug #15536: Fixed `yii\widgets\ActiveForm::init()` for call `parent::init()` (panchenkodv)
+- Bug #15540: Fixed `yii\db\ActiveRecord::with()` unable to use relation defined via attached behavior in case `asArray` is enabled (klimov-paul)
- Bug #15553: Fixed `yii\validators\NumberValidator` incorrectly validate resource (developeruz)
+- Bug #15621: Fixed `yii\web\User::getIdentity()` returning `null` if an exception had been thrown when it was called previously (brandonkelly)
+- Bug #15628: Fixed `yii\validators\DateValidator` to respect time when the `format` property is set to UNIX Epoch format (silverfire, gayHacker)
+- Bug #15644: Avoid wrong default selection on a dropdown, checkbox list, and radio list, when a option has a key equals to zero (berosoboy)
+- Bug #15658: Fixed `yii\filters\auth\HttpBasicAuth` not to switch identity, when user is already authenticated and identity does not get changed (silverfire)
+- Bug #15662: Fixed `yii\log\FileTarget` not to create log directory during init process (alexeevdv)
- Enh #3087: Added `yii\helpers\BaseHtml::error()` "errorSource" option to be able to customize errors display (yanggs07, developeruz, silverfire)
- Enh #3250: Added support for events partial wildcard matching (klimov-paul)
- Enh #5515: Added default value for `yii\behaviors\BlameableBehavior` for cases when the user is guest (dmirogin)
- Enh #6844: `yii\base\ArrayableTrait::toArray()` now allows recursive `$fields` and `$expand` (bboure)
+- Enh #7640: Implemented custom data types support. Added JSON support for MySQL and PostgreSQL, array support for PostgreSQL (silverfire, cebe)
- Enh #7988: Added `\yii\helpers\Console::errorSummary()` and `\yii\helpers\Json::errorSummary()` (developeruz)
- Enh #7996: Short syntax for verb in GroupUrlRule (schojniak, developeruz)
+- Enh #8092: ExistValidator for relations (developeruz)
+- Enh #8527: Added `yii\i18n\Locale` component having `getCurrencySymbol()` method (amarox, samdark)
- Enh #8752: Allow specify `$attributeNames` as a string for `yii\base\Model` `validate()` method (developeruz)
- Enh #9137: Added `Access-Control-Allow-Method` header for the OPTIONS request (developeruz)
- Enh #9253: Allow `variations` to be a string for `yii\filters\PageCache` and `yii\widgets\FragmentCache` (schojniak, developeruz)
+- Enh #9771: Assign hidden input with its own set of HTML options via `$hiddenOptions` in activeFileInput `$options` (HanafiAhmat)
+- Enh #10186: Use native `hash_equals` in `yii\base\Security::compareString()` if available, throw exception if non-strings are compared (aotd1, samdark)
+- Enh #11611: Added `BetweenColumnsCondition` to build SQL condition like `value BETWEEN col1 and col2` (silverfire)
- Enh #12623: Added `yii\helpers\StringHelper::matchWildcard()` replacing usage of `fnmatch()`, which may be unreliable (klimov-paul)
+- Enh #13019: Support JSON in SchemaBuilderTrait (zhukovra, undefinedor)
+- Enh #13425: Added caching of dynamically added URL rules with `yii\web\UrlManager::addRules()` (scriptcube, silverfire)
+- Enh #13465: Added `yii\helpers\FileHelper::findDirectories()` method (ArsSirek, developeruz)
+- Enh #13618: Active Record now resets related models after corresponding attributes updates (Kolyunya, rob006)
+- Enh #13679: Added `yii\behaviors\CacheableWidgetBehavior` (Kolyunya)
+- Enh #13814: MySQL unique index names can now contain spaces (df2)
+- Enh #13879: Added upsert support for `yii\db\QueryBuilder`, `yii\db\Command`, and `yii\db\Migration` (sergeymakinen)
- Enh #13919: Added option to add comment for created table to migration console command (mixartemev, developeruz)
+- Enh #13996: Added `yii\web\View::registerJsVar()` method that allows registering JavaScript variables (Eseperio, samdark)
- Enh #14043: Added `yii\helpers\IpHelper` (silverfire, cebe)
+- Enh #14254: add an option to specify whether validator is forced to always use master DB for `yii\validators\UniqueValidator` and `yii\validators\ExistValidator` (rossoneri, samdark)
- Enh #14355: Added ability to pass an empty array as a parameter in console command (developeruz)
+- Enh #14488: Added support for X-Forwarded-Host to `yii\web\Request`, fixed `getServerPort()` usage (si294r, samdark)
+- Enh #14538: Added `yii\behaviors\AttributeTypecastBehavior::typecastAfterSave` property (littlefuntik, silverfire)
+- Enh #14546: Added `dataDirectory` property into `BaseActiveFixture` (leandrogehlen)
- Enh #14568: Refactored migration templates to use `safeUp()` and `safeDown()` methods (Kolyunya)
+- Enh #14638: Added `yii\db\SchemaBuilderTrait::tinyInteger()` (rob006)
+- Enh #14643: Added `yii\web\ErrorAction::$layout` property to conveniently set layout from error action config (swods, cebe, samdark)
- Enh #14662: Added support for custom `Content-Type` specification to `yii\web\JsonResponseFormatter` (Kolyunya)
+- Enh #14732, #11218, #14810, #10855: It is now possible to pass `yii\db\Query` anywhere, where `yii\db\Expression` was supported (silverfire)
+- Enh #14806: Added $placeFooterAfterBody option for GridView (terehru)
- Enh #15024: `yii\web\Pjax` widget does not prevent CSS files from sending anymore because they are handled by client-side plugin correctly (onmotion)
+- Enh #15047: `yii\db\Query::select()` and `yii\db\Query::addSelect()` now check for duplicate column names (wapmorgan)
+- Enh #15076: Improve `yii\db\QueryBuilder::buildColumns()` to throw exception on invalid input (hiscaler)
+- Enh #15120: Refactored dynamic caching introducing `DynamicContentAwareInterface` and `DynamicContentAwareTrait` (sergeymakinen)
- Enh #15135: Automatic completion for help in bash and zsh (Valkeru)
+- Enh #15216: Added `yii\web\ErrorHandler::$traceLine` to allow opening file at line clicked in IDE (vladis84)
- Enh #15219: Added `yii\filters\auth\HttpHeaderAuth` (bboure)
- Enh #15221: Added support for specifying `--camelCase` console options in `--kebab-case` (brandonkelly)
- Enh #15221: Added support for the `--
+ *
+ * The return value will be casted to boolean if non-boolean was returned.
+ * @since 5.0.0
+ */
+ public function offsetExists($offset)
+ {
+ return isset($this->value[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ *
+ * @link http://php.net/manual/en/arrayaccess.offsetget.php
+ * @param mixed $offset
+ * The offset to retrieve.
+ *
+ * @return mixed Can return all value types.
+ * @since 5.0.0
+ */
+ public function offsetGet($offset)
+ {
+ return $this->value[$offset];
+ }
+
+ /**
+ * Offset to set
+ *
+ * @link http://php.net/manual/en/arrayaccess.offsetset.php
+ * @param mixed $offset
+ * @return void
+ * @since 5.0.0
+ */
+ public function offsetUnset($offset)
+ {
+ unset($this->value[$offset]);
+ }
+
+ /**
+ * Count elements of an object
+ *
+ * @link http://php.net/manual/en/countable.count.php
+ * @return int The custom count as an integer.
+ *
+ *
+ * The return value is cast to an integer.
+ * @since 5.1.0
+ */
+ public function count()
+ {
+ return count($this->value);
+ }
+}
diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php
index 46d8e16..8200417 100644
--- a/framework/db/BaseActiveRecord.php
+++ b/framework/db/BaseActiveRecord.php
@@ -8,9 +8,10 @@
namespace yii\db;
use Yii;
+use yii\base\InvalidArgumentException;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException;
-use yii\base\InvalidArgumentException;
+use yii\base\InvalidParamException;
use yii\base\Model;
use yii\base\ModelEvent;
use yii\base\NotSupportedException;
@@ -96,6 +97,10 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
* @var array related models indexed by the relation names
*/
private $_related = [];
+ /**
+ * @var array relation names indexed by their link attributes
+ */
+ private $_relationsDependencies = [];
/**
@@ -278,7 +283,9 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name];
- } elseif ($this->hasAttribute($name)) {
+ }
+
+ if ($this->hasAttribute($name)) {
return null;
}
@@ -287,6 +294,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
$value = parent::__get($name);
if ($value instanceof ActiveQueryInterface) {
+ $this->setRelationDependencies($name, $value);
return $this->_related[$name] = $value->findFor($name, $this);
}
@@ -302,6 +310,12 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
public function __set($name, $value)
{
if ($this->hasAttribute($name)) {
+ if (
+ !empty($this->_relationsDependencies[$name])
+ && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
+ ) {
+ $this->resetDependentRelations($name);
+ }
$this->_attributes[$name] = $value;
} else {
parent::__set($name, $value);
@@ -333,6 +347,9 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
{
if ($this->hasAttribute($name)) {
unset($this->_attributes[$name]);
+ if (!empty($this->_relationsDependencies[$name])) {
+ $this->resetDependentRelations($name);
+ }
} elseif (array_key_exists($name, $this->_related)) {
unset($this->_related[$name]);
} elseif ($this->getRelation($name, false) === null) {
@@ -497,6 +514,12 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
public function setAttribute($name, $value)
{
if ($this->hasAttribute($name)) {
+ if (
+ !empty($this->_relationsDependencies[$name])
+ && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
+ ) {
+ $this->resetDependentRelations($name);
+ }
$this->_attributes[$name] = $value;
} else {
throw new InvalidArgumentException(get_class($this) . ' has no attribute named "' . $name . '".');
@@ -1041,6 +1064,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
$this->_oldAttributes = $record->_oldAttributes;
$this->_related = [];
+ $this->_relationsDependencies = [];
$this->afterRefresh();
return true;
@@ -1160,6 +1184,8 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
}
$record->_oldAttributes = $record->_attributes;
+ $record->_related = [];
+ $record->_relationsDependencies = [];
}
/**
@@ -1209,7 +1235,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
$relation = $this->$getter();
} catch (UnknownMethodException $e) {
if ($throwException) {
- throw new InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
+ throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
}
return null;
@@ -1682,4 +1708,35 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
unset($this->$offset);
}
}
+
+ /**
+ * Resets dependent related models checking if their links contain specific attribute.
+ * @param string $attribute The changed attribute name.
+ */
+ private function resetDependentRelations($attribute)
+ {
+ foreach ($this->_relationsDependencies[$attribute] as $relation) {
+ unset($this->_related[$relation]);
+ }
+ unset($this->_relationsDependencies[$attribute]);
+ }
+
+ /**
+ * Sets relation dependencies for a property
+ * @param string $name property name
+ * @param ActiveQueryInterface $relation relation instance
+ */
+ private function setRelationDependencies($name, $relation)
+ {
+ if (empty($relation->via)) {
+ foreach ($relation->link as $attribute) {
+ $this->_relationsDependencies[$attribute][$name] = $name;
+ }
+ } elseif ($relation->via instanceof ActiveQueryInterface) {
+ $this->setRelationDependencies($name, $relation->via);
+ } elseif (is_array($relation->via)) {
+ list(, $viaQuery) = $relation->via;
+ $this->setRelationDependencies($name, $viaQuery);
+ }
+ }
}
diff --git a/framework/db/ColumnSchema.php b/framework/db/ColumnSchema.php
index 8ad4f87..d392aba 100644
--- a/framework/db/ColumnSchema.php
+++ b/framework/db/ColumnSchema.php
@@ -34,7 +34,7 @@ class ColumnSchema extends BaseObject
public $type;
/**
* @var string the PHP type of this column. Possible PHP types include:
- * `string`, `boolean`, `integer`, `double`.
+ * `string`, `boolean`, `integer`, `double`, `array`.
*/
public $phpType;
/**
@@ -114,12 +114,36 @@ class ColumnSchema extends BaseObject
*/
protected function typecast($value)
{
- if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING && $this->type !== Schema::TYPE_BINARY && $this->type !== Schema::TYPE_CHAR) {
+ if ($value === ''
+ && !in_array(
+ $this->type,
+ [
+ Schema::TYPE_TEXT,
+ Schema::TYPE_STRING,
+ Schema::TYPE_BINARY,
+ Schema::TYPE_CHAR
+ ],
+ true)
+ ) {
return null;
}
- if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression || $value instanceof Query) {
+
+ if ($value === null
+ || gettype($value) === $this->phpType
+ || $value instanceof ExpressionInterface
+ || $value instanceof Query
+ ) {
return $value;
}
+
+ if (is_array($value)
+ && count($value) === 2
+ && isset($value[1])
+ && in_array($value[1], $this->getPdoParamTypes(), true)
+ ) {
+ return new PdoValue($value[0], $value[1]);
+ }
+
switch ($this->phpType) {
case 'resource':
case 'string':
@@ -143,4 +167,12 @@ class ColumnSchema extends BaseObject
return $value;
}
+
+ /**
+ * @return int[] array of numbers that represent possible PDO parameter types
+ */
+ private function getPdoParamTypes()
+ {
+ return [\PDO::PARAM_BOOL, \PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_LOB, \PDO::PARAM_NULL, \PDO::PARAM_STMT];
+ }
}
diff --git a/framework/db/ColumnSchemaBuilder.php b/framework/db/ColumnSchemaBuilder.php
index d0c3064..edb28ef 100644
--- a/framework/db/ColumnSchemaBuilder.php
+++ b/framework/db/ColumnSchemaBuilder.php
@@ -91,6 +91,7 @@ class ColumnSchemaBuilder extends BaseObject
Schema::TYPE_CHAR => self::CATEGORY_STRING,
Schema::TYPE_STRING => self::CATEGORY_STRING,
Schema::TYPE_TEXT => self::CATEGORY_STRING,
+ Schema::TYPE_TINYINT => self::CATEGORY_NUMERIC,
Schema::TYPE_SMALLINT => self::CATEGORY_NUMERIC,
Schema::TYPE_INTEGER => self::CATEGORY_NUMERIC,
Schema::TYPE_BIGINT => self::CATEGORY_NUMERIC,
diff --git a/framework/db/Command.php b/framework/db/Command.php
index b6ab96c..37dee78 100644
--- a/framework/db/Command.php
+++ b/framework/db/Command.php
@@ -347,8 +347,8 @@ class Command extends Component
* @param array $values the values to be bound. This must be given in terms of an associative
* array with array keys being the parameter names, and array values the corresponding parameter values,
* e.g. `[':name' => 'John', ':age' => 25]`. By default, the PDO type of each value is determined
- * by its PHP type. You may explicitly specify the PDO type by using an array: `[value, type]`,
- * e.g. `[':name' => 'John', ':profile' => [$profile, \PDO::PARAM_LOB]]`.
+ * by its PHP type. You may explicitly specify the PDO type by using a [[yii\db\PdoValue]] class: `new PdoValue(value, type)`,
+ * e.g. `[':name' => 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
* @return $this the current command being executed
*/
public function bindValues($values)
@@ -359,9 +359,9 @@ class Command extends Component
$schema = $this->db->getSchema();
foreach ($values as $name => $value) {
- if (is_array($value)) {
- $this->_pendingParams[$name] = $value;
- $this->params[$name] = $value[0];
+ if ($value instanceof PdoValue) {
+ $this->_pendingParams[$name] = [$value->getValue(), $value->getType()];
+ $this->params[$name] = $value->getValue();
} else {
$type = $schema->getPdoType($value);
$this->_pendingParams[$name] = [$value, $type];
@@ -500,14 +500,52 @@ class Command extends Component
return $this->db->quoteSql($column);
}, $columns);
- $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows);
+ $params = [];
+ $sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows, $params);
$this->setRawSql($sql);
+ $this->bindValues($params);
return $this;
}
/**
+ * Creates a command to insert rows into a database table if
+ * they do not already exist (matching unique constraints),
+ * or update them if they do.
+ *
+ * For example,
+ *
+ * ```php
+ * $sql = $queryBuilder->upsert('pages', [
+ * 'name' => 'Front page',
+ * 'url' => 'http://example.com/', // url is unique
+ * 'visits' => 0,
+ * ], [
+ * 'visits' => new \yii\db\Expression('visits + 1'),
+ * ], $params);
+ * ```
+ *
+ * The method will properly escape the table and column names.
+ *
+ * @param string $table the table that new rows will be inserted into/updated in.
+ * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
+ * of [[Query]] to perform `INSERT INTO ... SELECT` SQL statement.
+ * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
+ * If `true` is passed, the column data will be updated to match the insert column data.
+ * If `false` is passed, no update will be performed if the column data already exists.
+ * @param array $params the parameters to be bound to the command.
+ * @return $this the command object itself.
+ * @since 2.0.14
+ */
+ public function upsert($table, $insertColumns, $updateColumns = true, $params = [])
+ {
+ $sql = $this->db->getQueryBuilder()->upsert($table, $insertColumns, $updateColumns, $params);
+
+ return $this->setSql($sql)->bindValues($params);
+ }
+
+ /**
* Creates an UPDATE command.
*
* For example,
@@ -995,7 +1033,7 @@ class Command extends Component
return $this->setSql($sql)->requireTableSchemaRefresh($viewName);
}
-
+
/**
* Drops a SQL View.
*
diff --git a/framework/db/Connection.php b/framework/db/Connection.php
index 1450596..9c72651 100644
--- a/framework/db/Connection.php
+++ b/framework/db/Connection.php
@@ -118,10 +118,11 @@ use yii\caching\CacheInterface;
* master available. This property is read-only.
* @property PDO $masterPdo The PDO instance for the currently active master connection. This property is
* read-only.
- * @property QueryBuilder $queryBuilder The query builder for the current DB connection. This property is
- * read-only.
+ * @property QueryBuilder $queryBuilder The query builder for the current DB connection. Note that the type of
+ * this property differs in getter and setter. See [[getQueryBuilder()]] and [[setQueryBuilder()]] for details.
* @property Schema $schema The schema information for the database opened by this connection. This property
* is read-only.
+ * @property string $serverVersion Server version as a string. This property is read-only.
* @property Connection $slave The currently active slave connection. `null` is returned if there is no slave
* available and `$fallbackToMaster` is false. This property is read-only.
* @property PDO $slavePdo The PDO instance for the currently active slave connection. `null` is returned if
@@ -295,7 +296,7 @@ class Connection extends Component
* Since version 2.0.14 [[$commandMap]] is used if this property is set to its default value.
* @see createCommand
* @since 2.0.7
- * @deprecated 2.0.14 Use [[$commandMap]] for precise configuration.
+ * @deprecated since 2.0.14. Use [[$commandMap]] for precise configuration.
*/
public $commandClass = Command::class;
/**
@@ -826,6 +827,17 @@ class Connection extends Component
}
/**
+ * Can be used to set [[QueryBuilder]] configuration via Connection configuration array.
+ *
+ * @param array $value the [[QueryBuilder]] properties to be configured.
+ * @since 2.0.14
+ */
+ public function setQueryBuilder($value)
+ {
+ Yii::configure($this->getQueryBuilder(), $value);
+ }
+
+ /**
* Obtains the schema information for the named table.
* @param string $name table name.
* @param bool $refresh whether to reload the table schema even if it is found in the cache.
@@ -937,6 +949,16 @@ class Connection extends Component
}
/**
+ * Returns a server version as a string comparable by [[\version_compare()]].
+ * @return string server version as a string.
+ * @since 2.0.14
+ */
+ public function getServerVersion()
+ {
+ return $this->getSchema()->getServerVersion();
+ }
+
+ /**
* Returns the PDO instance for the currently active slave connection.
* When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
* will be returned by this method.
diff --git a/framework/db/ConstraintFinderInterface.php b/framework/db/ConstraintFinderInterface.php
new file mode 100644
index 0000000..a22c3ba
--- /dev/null
+++ b/framework/db/ConstraintFinderInterface.php
@@ -0,0 +1,125 @@
+
+ * @since 2.0.14
+ */
+interface ConstraintFinderInterface
+{
+ /**
+ * Obtains the primary key for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return Constraint|null table primary key, `null` if the table has no primary key.
+ */
+ public function getTablePrimaryKey($name, $refresh = false);
+
+ /**
+ * Returns primary keys for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is `false`,
+ * cached data may be returned if available.
+ * @return Constraint[] primary keys for all tables in the database.
+ * Each array element is an instance of [[Constraint]] or its child class.
+ */
+ public function getSchemaPrimaryKeys($schema = '', $refresh = false);
+
+ /**
+ * Obtains the foreign keys information for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return ForeignKeyConstraint[] table foreign keys.
+ */
+ public function getTableForeignKeys($name, $refresh = false);
+
+ /**
+ * Returns foreign keys for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is false,
+ * cached data may be returned if available.
+ * @return ForeignKeyConstraint[][] foreign keys for all tables in the database.
+ * Each array element is an array of [[ForeignKeyConstraint]] or its child classes.
+ */
+ public function getSchemaForeignKeys($schema = '', $refresh = false);
+
+ /**
+ * Obtains the indexes information for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return IndexConstraint[] table indexes.
+ */
+ public function getTableIndexes($name, $refresh = false);
+
+ /**
+ * Returns indexes for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is false,
+ * cached data may be returned if available.
+ * @return IndexConstraint[][] indexes for all tables in the database.
+ * Each array element is an array of [[IndexConstraint]] or its child classes.
+ */
+ public function getSchemaIndexes($schema = '', $refresh = false);
+
+ /**
+ * Obtains the unique constraints information for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return Constraint[] table unique constraints.
+ */
+ public function getTableUniques($name, $refresh = false);
+
+ /**
+ * Returns unique constraints for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is false,
+ * cached data may be returned if available.
+ * @return Constraint[][] unique constraints for all tables in the database.
+ * Each array element is an array of [[Constraint]] or its child classes.
+ */
+ public function getSchemaUniques($schema = '', $refresh = false);
+
+ /**
+ * Obtains the check constraints information for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return CheckConstraint[] table check constraints.
+ */
+ public function getTableChecks($name, $refresh = false);
+
+ /**
+ * Returns check constraints for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is false,
+ * cached data may be returned if available.
+ * @return CheckConstraint[][] check constraints for all tables in the database.
+ * Each array element is an array of [[CheckConstraint]] or its child classes.
+ */
+ public function getSchemaChecks($schema = '', $refresh = false);
+
+ /**
+ * Obtains the default value constraints information for the named table.
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param bool $refresh whether to reload the information even if it is found in the cache.
+ * @return DefaultValueConstraint[] table default value constraints.
+ */
+ public function getTableDefaultValues($name, $refresh = false);
+
+ /**
+ * Returns default value constraints for all tables in the database.
+ * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema name.
+ * @param bool $refresh whether to fetch the latest available table schemas. If this is false,
+ * cached data may be returned if available.
+ * @return DefaultValueConstraint[] default value constraints for all tables in the database.
+ * Each array element is an array of [[DefaultValueConstraint]] or its child classes.
+ */
+ public function getSchemaDefaultValues($schema = '', $refresh = false);
+}
diff --git a/framework/db/Expression.php b/framework/db/Expression.php
index 28e79b2..351e5c9 100644
--- a/framework/db/Expression.php
+++ b/framework/db/Expression.php
@@ -28,7 +28,7 @@ namespace yii\db;
* @author Qiang Xue
* @since 2.0
*/
-class Expression extends \yii\base\BaseObject
+class Expression extends \yii\base\BaseObject implements ExpressionInterface
{
/**
* @var string the DB expression
@@ -57,7 +57,7 @@ class Expression extends \yii\base\BaseObject
/**
* String magic method.
- * @return string the DB expression
+ * @return string the DB expression.
*/
public function __toString()
{
diff --git a/framework/db/ExpressionBuilder.php b/framework/db/ExpressionBuilder.php
new file mode 100644
index 0000000..32637a7
--- /dev/null
+++ b/framework/db/ExpressionBuilder.php
@@ -0,0 +1,30 @@
+
+ * @since 2.0.14
+ */
+class ExpressionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * {@inheritdoc}
+ * @param Expression|ExpressionInterface $expression the expression to be built
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $params = array_merge($params, $expression->params);
+ return $expression->__toString();
+ }
+}
diff --git a/framework/db/ExpressionBuilderInterface.php b/framework/db/ExpressionBuilderInterface.php
new file mode 100644
index 0000000..7e65c67
--- /dev/null
+++ b/framework/db/ExpressionBuilderInterface.php
@@ -0,0 +1,28 @@
+
+ * @since 2.0.14
+ */
+interface ExpressionBuilderInterface
+{
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = []);
+}
diff --git a/framework/db/ExpressionBuilderTrait.php b/framework/db/ExpressionBuilderTrait.php
new file mode 100644
index 0000000..f2a1ce7
--- /dev/null
+++ b/framework/db/ExpressionBuilderTrait.php
@@ -0,0 +1,33 @@
+
+ * @since 2.0.14
+ */
+trait ExpressionBuilderTrait
+{
+ /**
+ * @var QueryBuilder
+ */
+ protected $queryBuilder;
+
+ /**
+ * ExpressionBuilderTrait constructor.
+ *
+ * @param QueryBuilder $queryBuilder
+ */
+ public function __construct(QueryBuilder $queryBuilder)
+ {
+ $this->queryBuilder = $queryBuilder;
+ }
+}
diff --git a/framework/db/ExpressionInterface.php b/framework/db/ExpressionInterface.php
new file mode 100644
index 0000000..73f8888
--- /dev/null
+++ b/framework/db/ExpressionInterface.php
@@ -0,0 +1,24 @@
+
+ * @since 2.0.14
+ */
+interface ExpressionInterface
+{
+}
diff --git a/framework/db/JsonExpression.php b/framework/db/JsonExpression.php
new file mode 100644
index 0000000..028fd06
--- /dev/null
+++ b/framework/db/JsonExpression.php
@@ -0,0 +1,73 @@
+ 1, 'b' => 2]); // will be encoded to '{"a": 1, "b": 2}'
+ * ```
+ *
+ * @author Dmytro Naumenko
+ * @since 2.0.14
+ */
+class JsonExpression implements ExpressionInterface
+{
+ const TYPE_JSON = 'json';
+ const TYPE_JSONB = 'jsonb';
+
+ /**
+ * @var mixed the value to be encoded to JSON.
+ * The value must be compatible with [\yii\helpers\Json::encode()|Json::encode()]] input requirements.
+ */
+ protected $value;
+ /**
+ * @var string|null Type of JSON, expression should be casted to. Defaults to `null`, meaning
+ * no explicit casting will be performed.
+ * This property will be encountered only for DBMSs that support different types of JSON.
+ * For example, PostgreSQL has `json` and `jsonb` types.
+ */
+ protected $type;
+
+
+ /**
+ * JsonExpression constructor.
+ *
+ * @param mixed $value the value to be encoded to JSON.
+ * The value must be compatible with [\yii\helpers\Json::encode()|Json::encode()]] requirements.
+ * @param string|null $type the type of the JSON. See [[JsonExpression::type]]
+ *
+ * @see type
+ */
+ public function __construct($value, $type = null)
+ {
+ $this->value = $value;
+ $this->type = $type;
+ }
+
+ /**
+ * @return mixed
+ * @see value
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @return null|string the type of JSON
+ * @see type
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+}
diff --git a/framework/db/Migration.php b/framework/db/Migration.php
index 6b3eb4a..d8705ed 100644
--- a/framework/db/Migration.php
+++ b/framework/db/Migration.php
@@ -240,6 +240,30 @@ class Migration extends Component implements MigrationInterface
}
/**
+ * Creates and executes a command to insert rows into a database table if
+ * they do not already exist (matching unique constraints),
+ * or update them if they do.
+ *
+ * The method will properly escape the column names, and bind the values to be inserted.
+ *
+ * @param string $table the table that new rows will be inserted into/updated in.
+ * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
+ * of [[Query]] to perform `INSERT INTO ... SELECT` SQL statement.
+ * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
+ * If `true` is passed, the column data will be updated to match the insert column data.
+ * If `false` is passed, no update will be performed if the column data already exists.
+ * @param array $params the parameters to be bound to the command.
+ * @return $this the command object itself.
+ * @since 2.0.14
+ */
+ public function upsert($table, $insertColumns, $updateColumns = true, $params = [])
+ {
+ $time = $this->beginCommand("upsert into $table");
+ $this->db->createCommand()->upsert($table, $insertColumns, $updateColumns, $params)->execute();
+ $this->endCommand($time);
+ }
+
+ /**
* Creates and executes an UPDATE SQL statement.
* The method will properly escape the column names and bind the values to be updated.
* @param string $table the table to be updated.
diff --git a/framework/db/PdoValue.php b/framework/db/PdoValue.php
new file mode 100644
index 0000000..f350a26
--- /dev/null
+++ b/framework/db/PdoValue.php
@@ -0,0 +1,65 @@
+ 'John', ':profile' => new PdoValue($profile, \PDO::PARAM_LOB)]`.
+ * ```
+ *
+ * To see possible types, check [PDO::PARAM_* constants](http://php.net/manual/en/pdo.constants.php).
+ *
+ * @see http://php.net/manual/en/pdostatement.bindparam.php
+ * @author Dmytro Naumenko
+ * @since 2.0.14
+ */
+final class PdoValue implements ExpressionInterface
+{
+ /**
+ * @var mixed
+ */
+ private $value;
+ /**
+ * @var int One of PDO_PARAM_* constants
+ * @see http://php.net/manual/en/pdo.constants.php
+ */
+ private $type;
+
+
+ /**
+ * PdoValue constructor.
+ *
+ * @param $value
+ * @param $type
+ */
+ public function __construct($value, $type)
+ {
+ $this->value = $value;
+ $this->type = $type;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @return int
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+}
diff --git a/framework/db/PdoValueBuilder.php b/framework/db/PdoValueBuilder.php
new file mode 100644
index 0000000..6e9d572
--- /dev/null
+++ b/framework/db/PdoValueBuilder.php
@@ -0,0 +1,31 @@
+
+ * @since 2.0.14
+ */
+class PdoValueBuilder implements ExpressionBuilderInterface
+{
+ const PARAM_PREFIX = ':pv';
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $placeholder = static::PARAM_PREFIX . count($params);
+ $params[$placeholder] = $expression;
+
+ return $placeholder;
+ }
+}
diff --git a/framework/db/Query.php b/framework/db/Query.php
index 2ecbab1..9c78228 100644
--- a/framework/db/Query.php
+++ b/framework/db/Query.php
@@ -9,8 +9,9 @@ namespace yii\db;
use Yii;
use yii\base\Component;
-use yii\base\InvalidConfigException;
use yii\base\InvalidArgumentException;
+use yii\helpers\ArrayHelper;
+use yii\base\InvalidConfigException;
/**
* Query represents a SELECT SQL statement in a way that is independent of DBMS.
@@ -47,7 +48,7 @@ use yii\base\InvalidArgumentException;
* @author Carsten Brandt
* @since 2.0
*/
-class Query extends Component implements QueryInterface
+class Query extends Component implements QueryInterface, ExpressionInterface
{
use QueryTrait;
@@ -97,7 +98,7 @@ class Query extends Component implements QueryInterface
*/
public $join;
/**
- * @var string|array|Expression the condition to be applied in the GROUP BY clause.
+ * @var string|array|ExpressionInterface the condition to be applied in the GROUP BY clause.
* It can be either a string or an array. Please refer to [[where()]] on how to specify the condition.
*/
public $having;
@@ -114,6 +115,21 @@ class Query extends Component implements QueryInterface
* For example, `[':name' => 'Dan', ':age' => 31]`.
*/
public $params = [];
+ /**
+ * @var int|true the default number of seconds that query results can remain valid in cache.
+ * Use 0 to indicate that the cached data will never expire.
+ * Use a negative number to indicate that query cache should not be used.
+ * Use boolean `true` to indicate that [[Connection::queryCacheDuration]] should be used.
+ * @see cache()
+ * @since 2.0.14
+ */
+ public $queryCacheDuration;
+ /**
+ * @var \yii\caching\Dependency the dependency to be associated with the cached query result for this query
+ * @see cache()
+ * @since 2.0.14
+ */
+ public $queryCacheDependency;
/**
@@ -129,7 +145,10 @@ class Query extends Component implements QueryInterface
}
[$sql, $params] = $db->getQueryBuilder()->build($this);
- return $db->createCommand($sql, $params);
+ $command = $db->createCommand($sql, $params);
+ $this->setCommandCache($command);
+
+ return $command;
}
/**
@@ -233,12 +252,7 @@ class Query extends Component implements QueryInterface
}
$result = [];
foreach ($rows as $row) {
- if (is_string($this->indexBy)) {
- $key = $row[$this->indexBy];
- } else {
- $key = call_user_func($this->indexBy, $row);
- }
- $result[$key] = $row;
+ $result[ArrayHelper::getValue($row, $this->indexBy)] = $row;
}
return $result;
@@ -414,7 +428,7 @@ class Query extends Component implements QueryInterface
/**
* Queries a scalar value by setting [[select]] first.
* Restores the value of select to make this query reusable.
- * @param string|Expression $selectExpression
+ * @param string|ExpressionInterface $selectExpression
* @param Connection|null $db
* @return bool|string
*/
@@ -449,11 +463,13 @@ class Query extends Component implements QueryInterface
return $command->queryScalar();
}
- return (new self())
+ $command = (new self())
->select([$selectExpression])
->from(['c' => $this])
- ->createCommand($db)
- ->queryScalar();
+ ->createCommand($db);
+ $this->setCommandCache($command);
+
+ return $command->queryScalar();
}
/**
@@ -567,12 +583,12 @@ PATTERN;
/**
* Sets the SELECT part of the query.
- * @param string|array|Expression $columns the columns to be selected.
+ * @param string|array|ExpressionInterface $columns the columns to be selected.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* Columns can be prefixed with table names (e.g. "user.id") and/or contain column aliases (e.g. "user.id AS user_id").
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression). A DB expression may also be passed in form of
- * an [[Expression]] object.
+ * an [[ExpressionInterface]] object.
*
* Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should
* use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts.
@@ -589,12 +605,13 @@ PATTERN;
*/
public function select($columns, $option = null)
{
- if ($columns instanceof Expression) {
+ if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
- $this->select = $columns;
+ $this->select = [];
+ $this->select = $this->getUniqueColumns($columns);
$this->selectOption = $option;
return $this;
}
@@ -609,18 +626,19 @@ PATTERN;
* $query->addSelect(["*", "CONCAT(first_name, ' ', last_name) AS full_name"])->one();
* ```
*
- * @param string|array|Expression $columns the columns to add to the select. See [[select()]] for more
+ * @param string|array|ExpressionInterface $columns the columns to add to the select. See [[select()]] for more
* details about the format of this parameter.
* @return $this the query object itself
* @see select()
*/
public function addSelect($columns)
{
- if ($columns instanceof Expression) {
+ if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
+ $columns = $this->getUniqueColumns($columns);
if ($this->select === null) {
$this->select = $columns;
} else {
@@ -631,6 +649,51 @@ PATTERN;
}
/**
+ * Returns unique column names excluding duplicates.
+ * Columns to be removed:
+ * - if column definition already present in SELECT part with same alias
+ * - if column definition without alias already present in SELECT part without alias too
+ * @param array $columns the columns to be merged to the select.
+ * @since 2.0.14
+ */
+ protected function getUniqueColumns($columns)
+ {
+ $columns = array_unique($columns);
+ $unaliasedColumns = $this->getUnaliasedColumnsFromSelect();
+
+ foreach ($columns as $columnAlias => $columnDefinition) {
+ if ($columnDefinition instanceof Query) {
+ continue;
+ }
+
+ if (
+ (is_string($columnAlias) && isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition)
+ || (is_integer($columnAlias) && in_array($columnDefinition, $unaliasedColumns))
+ ) {
+ unset($columns[$columnAlias]);
+ }
+ }
+ return $columns;
+ }
+
+ /**
+ * @return array List of columns without aliases from SELECT statement.
+ * @since 2.0.14
+ */
+ protected function getUnaliasedColumnsFromSelect()
+ {
+ $result = [];
+ if (is_array($this->select)) {
+ foreach ($this->select as $name => $value) {
+ if (is_integer($name)) {
+ $result[] = $value;
+ }
+ }
+ }
+ return array_unique($result);
+ }
+
+ /**
* Sets the value indicating whether to SELECT DISTINCT or not.
* @param bool $value whether to SELECT DISTINCT or not.
* @return $this the query object itself
@@ -643,7 +706,7 @@ PATTERN;
/**
* Sets the FROM part of the query.
- * @param string|array|Expression $tables the table(s) to be selected from. This can be either a string (e.g. `'user'`)
+ * @param string|array|ExpressionInterface $tables the table(s) to be selected from. This can be either a string (e.g. `'user'`)
* or an array (e.g. `['user', 'profile']`) specifying one or several table names.
* Table names can contain schema prefixes (e.g. `'public.user'`) and/or table aliases (e.g. `'user u'`).
* The method will automatically quote the table names unless it contains some parenthesis
@@ -655,7 +718,7 @@ PATTERN;
* Use a Query object to represent a sub-query. In this case, the corresponding array key will be used
* as the alias for the sub-query.
*
- * To specify the `FROM` part in plain SQL, you may pass an instance of [[Expression]].
+ * To specify the `FROM` part in plain SQL, you may pass an instance of [[ExpressionInterface]].
*
* Here are some examples:
*
@@ -677,6 +740,9 @@ PATTERN;
*/
public function from($tables)
{
+ if ($tables instanceof Expression) {
+ $tables = [$tables];
+ }
if (is_string($tables)) {
$tables = preg_split('/\s*,\s*/', trim($tables), -1, PREG_SPLIT_NO_EMPTY);
}
@@ -694,7 +760,7 @@ PATTERN;
*
* {@inheritdoc}
*
- * @param string|array|Expression $condition the conditions that should be put in the WHERE part.
+ * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
* @see andWhere()
@@ -711,7 +777,7 @@ PATTERN;
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the `AND` operator.
- * @param string|array|Expression $condition the new WHERE condition. Please refer to [[where()]]
+ * @param string|array|ExpressionInterface $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -734,7 +800,7 @@ PATTERN;
/**
* Adds an additional WHERE condition to the existing one.
* The new condition and the existing one will be joined using the `OR` operator.
- * @param string|array|Expression $condition the new WHERE condition. Please refer to [[where()]]
+ * @param string|array|ExpressionInterface $condition the new WHERE condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -900,7 +966,7 @@ PATTERN;
/**
* Sets the GROUP BY part of the query.
- * @param string|array|Expression $columns the columns to be grouped by.
+ * @param string|array|ExpressionInterface $columns the columns to be grouped by.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
@@ -909,13 +975,14 @@ PATTERN;
* to represent the group-by information. Otherwise, the method will not be able to correctly determine
* the group-by columns.
*
- * Since version 2.0.7, an [[Expression]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
+ * Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
+ * Since version 2.0.14, an [[ExpressionInterface]] object can be passed as well.
* @return $this the query object itself
* @see addGroupBy()
*/
public function groupBy($columns)
{
- if ($columns instanceof Expression) {
+ if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -936,12 +1003,13 @@ PATTERN;
* the group-by columns.
*
* Since version 2.0.7, an [[Expression]] object can be passed to specify the GROUP BY part explicitly in plain SQL.
+ * Since version 2.0.14, an [[ExpressionInterface]] object can be passed as well.
* @return $this the query object itself
* @see groupBy()
*/
public function addGroupBy($columns)
{
- if ($columns instanceof Expression) {
+ if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
@@ -957,7 +1025,7 @@ PATTERN;
/**
* Sets the HAVING part of the query.
- * @param string|array|Expression $condition the conditions to be put after HAVING.
+ * @param string|array|ExpressionInterface $condition the conditions to be put after HAVING.
* Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -974,7 +1042,7 @@ PATTERN;
/**
* Adds an additional HAVING condition to the existing one.
* The new condition and the existing one will be joined using the `AND` operator.
- * @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]]
+ * @param string|array|ExpressionInterface $condition the new HAVING condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -995,7 +1063,7 @@ PATTERN;
/**
* Adds an additional HAVING condition to the existing one.
* The new condition and the existing one will be joined using the `OR` operator.
- * @param string|array|Expression $condition the new HAVING condition. Please refer to [[where()]]
+ * @param string|array|ExpressionInterface $condition the new HAVING condition. Please refer to [[where()]]
* on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query.
* @return $this the query object itself
@@ -1153,6 +1221,52 @@ PATTERN;
}
/**
+ * Enables query cache for this Query.
+ * @param int|true $duration the number of seconds that query results can remain valid in cache.
+ * Use 0 to indicate that the cached data will never expire.
+ * Use a negative number to indicate that query cache should not be used.
+ * Use boolean `true` to indicate that [[Connection::queryCacheDuration]] should be used.
+ * Defaults to `true`.
+ * @param \yii\caching\Dependency $dependency the cache dependency associated with the cached result.
+ * @return $this the Query object itself
+ * @since 2.0.14
+ */
+ public function cache($duration = true, $dependency = null)
+ {
+ $this->queryCacheDuration = $duration;
+ $this->queryCacheDependency = $dependency;
+ return $this;
+ }
+
+ /**
+ * Disables query cache for this Query.
+ * @return $this the Query object itself
+ * @since 2.0.14
+ */
+ public function noCache()
+ {
+ $this->queryCacheDuration = -1;
+ return $this;
+ }
+
+ /**
+ * Sets $command cache, if this query has enabled caching.
+ *
+ * @param Command $command
+ * @return Command
+ * @since 2.0.14
+ */
+ protected function setCommandCache($command)
+ {
+ if ($this->queryCacheDuration !== null || $this->queryCacheDependency !== null) {
+ $duration = $this->queryCacheDuration === true ? null : $this->queryCacheDuration;
+ $command->cache($duration, $this->queryCacheDependency);
+ }
+
+ return $command;
+ }
+
+ /**
* Creates a new Query object and copies its property values from an existing one.
* The properties being copies are the ones to be used by query builders.
* @param Query $from the source query object
@@ -1177,4 +1291,13 @@ PATTERN;
'params' => $from->params,
]);
}
+
+ /**
+ * Returns the SQL representation of Query
+ * @return string
+ */
+ public function __toString()
+ {
+ return serialize($this);
+ }
}
diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php
index 8b79209..b828989 100644
--- a/framework/db/QueryBuilder.php
+++ b/framework/db/QueryBuilder.php
@@ -9,7 +9,8 @@ namespace yii\db;
use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
-use yii\helpers\ArrayHelper;
+use yii\db\conditions\ConditionInterface;
+use yii\db\conditions\HashCondition;
use yii\helpers\StringHelper;
/**
@@ -21,6 +22,9 @@ use yii\helpers\StringHelper;
*
* For more details and usage information on QueryBuilder, see the [guide article on query builders](guide:db-query-builder).
*
+ * @property string[] $expressionBuilders Array of builders that should be merged with the pre-defined ones
+ * in [[expressionBuilders]] property. This property is write-only.
+ *
* @author Qiang Xue
* @since 2.0
*/
@@ -50,38 +54,53 @@ class QueryBuilder extends \yii\base\BaseObject
/**
* @var array map of query condition to builder methods.
* These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
+ * @deprecated since 2.0.14. Is not used, will be dropped in 2.1.0.
*/
- protected $conditionBuilders = [
- 'NOT' => 'buildNotCondition',
- 'AND' => 'buildAndCondition',
- 'OR' => 'buildAndCondition',
- 'BETWEEN' => 'buildBetweenCondition',
- 'NOT BETWEEN' => 'buildBetweenCondition',
- 'IN' => 'buildInCondition',
- 'NOT IN' => 'buildInCondition',
- 'LIKE' => 'buildLikeCondition',
- 'NOT LIKE' => 'buildLikeCondition',
- 'OR LIKE' => 'buildLikeCondition',
- 'OR NOT LIKE' => 'buildLikeCondition',
- 'EXISTS' => 'buildExistsCondition',
- 'NOT EXISTS' => 'buildExistsCondition',
- ];
+ protected $conditionBuilders = [];
/**
- * @var array map of chars to their replacements in LIKE conditions.
- * By default it's configured to escape `%`, `_` and `\` with `\`.
- * @since 2.0.12.
+ * @var array map of condition aliases to condition classes. For example:
+ *
+ * ```php
+ * return [
+ * 'LIKE' => yii\db\condition\LikeCondition::class,
+ * ];
+ * ```
+ *
+ * This property is used by [[createConditionFromArray]] method.
+ * See default condition classes list in [[defaultConditionClasses()]] method.
+ *
+ * In case you want to add custom conditions support, use the [[setConditionClasses()]] method.
+ *
+ * @see setConditonClasses()
+ * @see defaultConditionClasses()
+ * @since 2.0.14
*/
- protected $likeEscapingReplacements = [
- '%' => '\%',
- '_' => '\_',
- '\\' => '\\\\',
- ];
+ protected $conditionClasses = [];
/**
- * @var string|null character used to escape special characters in LIKE conditions.
- * By default it's assumed to be `\`.
- * @since 2.0.12
+ * @var string[]|ExpressionBuilderInterface[] maps expression class to expression builder class.
+ * For example:
+ *
+ * ```php
+ * [
+ * yii\db\Expression::class => yii\db\ExpressionBuilder::class
+ * ]
+ * ```
+ * This property is mainly used by [[buildExpression()]] to build SQL expressions form expression objects.
+ * See default values in [[defaultExpressionBuilders()]] method.
+ *
+ *
+ * To override existing builders or add custom, use [[setExpressionBuilder()]] method. New items will be added
+ * to the end of this array.
+ *
+ * To find a builder, [[buildExpression()]] will check the expression class for its exact presence in this map.
+ * In case it is NOT present, the array will be iterated in reverse direction, checking whether the expression
+ * extends the class, defined in this map.
+ *
+ * @see setExpressionBuilders()
+ * @see defaultExpressionBuilders()
+ * @since 2.0.14
*/
- protected $likeEscapeCharacter;
+ protected $expressionBuilders = [];
/**
@@ -96,7 +115,87 @@ class QueryBuilder extends \yii\base\BaseObject
}
/**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ parent::init();
+
+ $this->expressionBuilders = array_merge($this->defaultExpressionBuilders(), $this->expressionBuilders);
+ $this->conditionClasses = array_merge($this->defaultConditionClasses(), $this->conditionClasses);
+ }
+
+ /**
+ * Contains array of default condition classes. Extend this method, if you want to change
+ * default condition classes for the query builder. See [[conditionClasses]] docs for details.
+ *
+ * @return array
+ * @see conditionClasses
+ * @since 2.0.14
+ */
+ protected function defaultConditionClasses()
+ {
+ return [
+ 'NOT' => 'yii\db\conditions\NotCondition',
+ 'AND' => 'yii\db\conditions\AndCondition',
+ 'OR' => 'yii\db\conditions\OrCondition',
+ 'BETWEEN' => 'yii\db\conditions\BetweenCondition',
+ 'NOT BETWEEN' => 'yii\db\conditions\BetweenCondition',
+ 'IN' => 'yii\db\conditions\InCondition',
+ 'NOT IN' => 'yii\db\conditions\InCondition',
+ 'LIKE' => 'yii\db\conditions\LikeCondition',
+ 'NOT LIKE' => 'yii\db\conditions\LikeCondition',
+ 'OR LIKE' => 'yii\db\conditions\LikeCondition',
+ 'OR NOT LIKE' => 'yii\db\conditions\LikeCondition',
+ 'EXISTS' => 'yii\db\conditions\ExistsCondition',
+ 'NOT EXISTS' => 'yii\db\conditions\ExistsCondition',
+ ];
+ }
+
+ /**
+ * Contains array of default expression builders. Extend this method and override it, if you want to change
+ * default expression builders for this query builder. See [[expressionBuilders]] docs for details.
+ *
+ * @return array
+ * @see $expressionBuilders
+ * @since 2.0.14
+ */
+ protected function defaultExpressionBuilders()
+ {
+ return [
+ 'yii\db\Query' => 'yii\db\QueryExpressionBuilder',
+ 'yii\db\PdoValue' => 'yii\db\PdoValueBuilder',
+ 'yii\db\Expression' => 'yii\db\ExpressionBuilder',
+ 'yii\db\conditions\ConjunctionCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
+ 'yii\db\conditions\NotCondition' => 'yii\db\conditions\NotConditionBuilder',
+ 'yii\db\conditions\AndCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
+ 'yii\db\conditions\OrCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
+ 'yii\db\conditions\BetweenCondition' => 'yii\db\conditions\BetweenConditionBuilder',
+ 'yii\db\conditions\InCondition' => 'yii\db\conditions\InConditionBuilder',
+ 'yii\db\conditions\LikeCondition' => 'yii\db\conditions\LikeConditionBuilder',
+ 'yii\db\conditions\ExistsCondition' => 'yii\db\conditions\ExistsConditionBuilder',
+ 'yii\db\conditions\SimpleCondition' => 'yii\db\conditions\SimpleConditionBuilder',
+ 'yii\db\conditions\HashCondition' => 'yii\db\conditions\HashConditionBuilder',
+ 'yii\db\conditions\BetweenColumnsCondition' => 'yii\db\conditions\BetweenColumnsConditionBuilder',
+ ];
+ }
+
+ /**
+ * Setter for [[expressionBuilders]] property.
+ *
+ * @param string[] $builders array of builders that should be merged with the pre-defined ones
+ * in [[expressionBuilders]] property.
+ * @since 2.0.14
+ * @see expressionBuilders
+ */
+ public function setExpressionBuilders($builders)
+ {
+ $this->expressionBuilders = array_merge($this->expressionBuilders, $builders);
+ }
+
+ /**
* Generates a SELECT SQL statement from a [[Query]] object.
+ *
* @param Query $query the [[Query]] object from which the SQL statement will be generated.
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
@@ -131,8 +230,65 @@ class QueryBuilder extends \yii\base\BaseObject
}
/**
- * Creates an INSERT SQL statement.
+ * Builds given $expression
+ *
+ * @param ExpressionInterface $expression the expression to be built
+ * @param array $params the parameters to be bound to the generated SQL statement. These parameters will
+ * be included in the result with the additional parameters generated during the expression building process.
+ * @return string the SQL statement that will not be neither quoted nor encoded before passing to DBMS
+ * @see ExpressionInterface
+ * @see ExpressionBuilderInterface
+ * @see expressionBuilders
+ * @since 2.0.14
+ * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
+ */
+ public function buildExpression(ExpressionInterface $expression, &$params = [])
+ {
+ $builder = $this->getExpressionBuilder($expression);
+
+ return $builder->build($expression, $params);
+ }
+
+ /**
+ * Gets object of [[ExpressionBuilderInterface]] that is suitable for $expression.
+ * Uses [[expressionBuilders]] array to find a suitable builder class.
*
+ * @param ExpressionInterface $expression
+ * @return ExpressionBuilderInterface
+ * @see expressionBuilders
+ * @since 2.0.14
+ * @throws InvalidArgumentException when $expression building is not supported by this QueryBuilder.
+ */
+ public function getExpressionBuilder(ExpressionInterface $expression)
+ {
+ $className = get_class($expression);
+
+ if (!isset($this->expressionBuilders[$className])) {
+ foreach (array_reverse($this->expressionBuilders) as $expressionClass => $builderClass) {
+ if (is_subclass_of($expression, $expressionClass)) {
+ $this->expressionBuilders[$className] = $builderClass;
+ break;
+ }
+ }
+
+ if (!isset($this->expressionBuilders[$className])) {
+ throw new InvalidArgumentException('Expression of class ' . $className . ' can not be built in ' . get_class($this));
+ }
+ }
+
+ if ($this->expressionBuilders[$className] === __CLASS__) {
+ return $this;
+ }
+
+ if (!is_object($this->expressionBuilders[$className])) {
+ $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
+ }
+
+ return $this->expressionBuilders[$className];
+ }
+
+ /**
+ * Creates an INSERT SQL statement.
* For example,
*
* ```php
@@ -145,7 +301,7 @@ class QueryBuilder extends \yii\base\BaseObject
* The method will properly escape the table and column names.
*
* @param string $table the table that new rows will be inserted into.
- * @param array|\yii\db\Query $columns the column data (name => value) to be inserted into the table or instance
+ * @param array|Query $columns the column data (name => value) to be inserted into the table or instance
* of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
* Passing of [[yii\db\Query|Query]] is available since version 2.0.11.
* @param array $params the binding parameters that will be generated by this method.
@@ -154,49 +310,59 @@ class QueryBuilder extends \yii\base\BaseObject
*/
public function insert($table, $columns, &$params)
{
+ list($names, $placeholders, $values, $params) = $this->prepareInsertValues($table, $columns, $params);
+ return 'INSERT INTO ' . $this->db->quoteTableName($table)
+ . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
+ . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
+ }
+
+ /**
+ * Prepares a `VALUES` part for an `INSERT` SQL statement.
+ *
+ * @param string $table the table that new rows will be inserted into.
+ * @param array|Query $columns the column data (name => value) to be inserted into the table or instance
+ * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
+ * @param array $params the binding parameters that will be generated by this method.
+ * They should be bound to the DB command later.
+ * @return array array of column names, placeholders, values and params.
+ * @since 2.0.14
+ */
+ protected function prepareInsertValues($table, $columns, $params = [])
+ {
$schema = $this->db->getSchema();
- if (($tableSchema = $schema->getTableSchema($table)) !== null) {
- $columnSchemas = $tableSchema->columns;
- } else {
- $columnSchemas = [];
- }
+ $tableSchema = $schema->getTableSchema($table);
+ $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
$names = [];
$placeholders = [];
$values = ' DEFAULT VALUES';
- if ($columns instanceof \yii\db\Query) {
- [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
+ if ($columns instanceof Query) {
+ list($names, $values, $params) = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
} else {
foreach ($columns as $name => $value) {
$names[] = $schema->quoteColumnName($name);
- if ($value instanceof Expression) {
- $placeholders[] = $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
+ $value = isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
+
+ if ($value instanceof ExpressionInterface) {
+ $placeholders[] = $this->buildExpression($value, $params);
} elseif ($value instanceof \yii\db\Query) {
[$sql, $params] = $this->build($value, $params);
$placeholders[] = "($sql)";
} else {
- $phName = self::PARAM_PREFIX . count($params);
- $placeholders[] = $phName;
- $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
+ $placeholders[] = $this->bindParam($value, $params);
}
}
}
-
- return 'INSERT INTO ' . $schema->quoteTableName($table)
- . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
- . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
+ return [$names, $placeholders, $values, $params];
}
/**
* Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement.
*
- * @param \yii\db\Query $columns Object, which represents select query.
+ * @param Query $columns Object, which represents select query.
* @param \yii\db\Schema $schema Schema object to quote column name.
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
- * @return array
+ * @return array array of column names, values and params.
* @throws InvalidArgumentException if query's select does not contain named parameters only.
* @since 2.0.11
*/
@@ -242,9 +408,10 @@ class QueryBuilder extends \yii\base\BaseObject
* @param string $table the table that new rows will be inserted into.
* @param array $columns the column names
* @param array|\Generator $rows the rows to be batch inserted into the table
+ * @param array $params the binding parameters. This parameter exists since 2.0.14
* @return string the batch INSERT SQL statement
*/
- public function batchInsert($table, $columns, $rows)
+ public function batchInsert($table, $columns, $rows, &$params = [])
{
if (empty($rows)) {
return '';
@@ -261,7 +428,7 @@ class QueryBuilder extends \yii\base\BaseObject
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
- if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
+ if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -273,6 +440,8 @@ class QueryBuilder extends \yii\base\BaseObject
$value = 0;
} elseif ($value === null) {
$value = 'NULL';
+ } elseif ($value instanceof ExpressionInterface) {
+ $value = $this->buildExpression($value, $params);
}
$vs[] = $value;
}
@@ -291,6 +460,115 @@ class QueryBuilder extends \yii\base\BaseObject
}
/**
+ * Creates an SQL statement to insert rows into a database table if
+ * they do not already exist (matching unique constraints),
+ * or update them if they do.
+ *
+ * For example,
+ *
+ * ```php
+ * $sql = $queryBuilder->upsert('pages', [
+ * 'name' => 'Front page',
+ * 'url' => 'http://example.com/', // url is unique
+ * 'visits' => 0,
+ * ], [
+ * 'visits' => new \yii\db\Expression('visits + 1'),
+ * ], $params);
+ * ```
+ *
+ * The method will properly escape the table and column names.
+ *
+ * @param string $table the table that new rows will be inserted into/updated in.
+ * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
+ * of [[Query]] to perform `INSERT INTO ... SELECT` SQL statement.
+ * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
+ * If `true` is passed, the column data will be updated to match the insert column data.
+ * If `false` is passed, no update will be performed if the column data already exists.
+ * @param array $params the binding parameters that will be generated by this method.
+ * They should be bound to the DB command later.
+ * @return string the resulting SQL.
+ * @throws NotSupportedException if this is not supported by the underlying DBMS.
+ * @since 2.0.14
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ throw new NotSupportedException($this->db->getDriverName() . ' does not support upsert statements.');
+ }
+
+ /**
+ * @param string $table
+ * @param array|Query $insertColumns
+ * @param array|bool $updateColumns
+ * @param Constraint[] $constraints this parameter recieves a matched constraint list.
+ * The constraints will be unique by their column names.
+ * @return array
+ * @since 2.0.14
+ */
+ protected function prepareUpsertColumns($table, $insertColumns, $updateColumns, &$constraints = [])
+ {
+ if ($insertColumns instanceof Query) {
+ list($insertNames) = $this->prepareInsertSelectSubQuery($insertColumns, $this->db->getSchema());
+ } else {
+ $insertNames = array_map([$this->db, 'quoteColumnName'], array_keys($insertColumns));
+ }
+ $uniqueNames = $this->getTableUniqueColumnNames($table, $insertNames, $constraints);
+ $uniqueNames = array_map([$this->db, 'quoteColumnName'], $uniqueNames);
+ if ($updateColumns !== true) {
+ return [$uniqueNames, $insertNames, null];
+ }
+
+ return [$uniqueNames, $insertNames, array_diff($insertNames, $uniqueNames)];
+ }
+
+ /**
+ * Returns all column names belonging to constraints enforcing uniqueness (`PRIMARY KEY`, `UNIQUE INDEX`, etc.)
+ * for the named table removing constraints which did not cover the specified column list.
+ * The column list will be unique by column names.
+ *
+ * @param string $name table name. The table name may contain schema name if any. Do not quote the table name.
+ * @param string[] $columns source column list.
+ * @param Constraint[] $constraints this parameter optionally recieves a matched constraint list.
+ * The constraints will be unique by their column names.
+ * @return string[] column list.
+ */
+ private function getTableUniqueColumnNames($name, $columns, &$constraints = [])
+ {
+ $schema = $this->db->getSchema();
+ if (!$schema instanceof ConstraintFinderInterface) {
+ return [];
+ }
+
+ $constraints = [];
+ $primaryKey = $schema->getTablePrimaryKey($name);
+ if ($primaryKey !== null) {
+ $constraints[] = $primaryKey;
+ }
+ foreach ($schema->getTableIndexes($name) as $constraint) {
+ if ($constraint->isUnique) {
+ $constraints[] = $constraint;
+ }
+ }
+ $constraints = array_merge($constraints, $schema->getTableUniques($name));
+ // Remove duplicates
+ $constraints = array_combine(array_map(function (Constraint $constraint) {
+ $columns = $constraint->columnNames;
+ sort($columns, SORT_STRING);
+ return json_encode($columns);
+ }, $constraints), $constraints);
+ $columnNames = [];
+ // Remove all constraints which do not cover the specified column list
+ $constraints = array_values(array_filter($constraints, function (Constraint $constraint) use ($schema, $columns, &$columnNames) {
+ $constraintColumnNames = array_map([$schema, 'quoteColumnName'], $constraint->columnNames);
+ $result = !array_diff($constraintColumnNames, $columns);
+ if ($result) {
+ $columnNames = array_merge($columnNames, $constraintColumnNames);
+ }
+ return $result;
+ }));
+ return array_unique($columnNames);
+ }
+
+ /**
* Creates an UPDATE SQL statement.
*
* For example,
@@ -312,30 +590,38 @@ class QueryBuilder extends \yii\base\BaseObject
*/
public function update($table, $columns, $condition, &$params)
{
- if (($tableSchema = $this->db->getTableSchema($table)) !== null) {
- $columnSchemas = $tableSchema->columns;
- } else {
- $columnSchemas = [];
- }
+ list($lines, $params) = $this->prepareUpdateSets($table, $columns, $params);
+ $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
+ $where = $this->buildWhere($condition, $params);
+ return $where === '' ? $sql : $sql . ' ' . $where;
+ }
- $lines = [];
+ /**
+ * Prepares a `SET` parts for an `UPDATE` SQL statement.
+ * @param string $table the table to be updated.
+ * @param array $columns the column data (name => value) to be updated.
+ * @param array $params the binding parameters that will be modified by this method
+ * so that they can be bound to the DB command later.
+ * @return array an array `SET` parts for an `UPDATE` SQL statement (the first array element) and params (the second array element).
+ * @since 2.0.14
+ */
+ protected function prepareUpdateSets($table, $columns, $params = [])
+ {
+ $tableSchema = $this->db->getTableSchema($table);
+ $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
+ $sets = [];
foreach ($columns as $name => $value) {
- if ($value instanceof Expression) {
- $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
+ if ($value instanceof ExpressionInterface) {
+ $sets[] = $this->db->quoteColumnName($name) . '=' . $this->buildExpression($value, $params);
} else {
- $phName = self::PARAM_PREFIX . count($params);
- $lines[] = $this->db->quoteColumnName($name) . '=' . $phName;
- $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
+ $phName = $this->bindParam(
+ isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value,
+ $params
+ );
+ $sets[] = $this->db->quoteColumnName($name) . '=' . $phName;
}
}
-
- $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines);
- $where = $this->buildWhere($condition, $params);
-
- return $where === '' ? $sql : $sql . ' ' . $where;
+ return [$sets, $params];
}
/**
@@ -904,13 +1190,12 @@ class QueryBuilder extends \yii\base\BaseObject
}
foreach ($columns as $i => $column) {
- if ($column instanceof Expression) {
+ if ($column instanceof ExpressionInterface) {
if (is_int($i)) {
- $columns[$i] = $column->expression;
+ $columns[$i] = $this->buildExpression($column, $params);
} else {
- $columns[$i] = $column->expression . ' AS ' . $this->db->quoteColumnName($i);
+ $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->db->quoteColumnName($i);
}
- $params = array_merge($params, $column->params);
} elseif ($column instanceof Query) {
[$sql, $params] = $this->build($column, $params);
$columns[$i] = "($sql) AS " . $this->db->quoteColumnName($i);
@@ -1032,8 +1317,8 @@ class QueryBuilder extends \yii\base\BaseObject
return '';
}
foreach ($columns as $i => $column) {
- if ($column instanceof Expression) {
- $columns[$i] = $column->expression;
+ if ($column instanceof ExpressionInterface) {
+ $columns[$i] = $this->buildExpression($column);
$params = array_merge($params, $column->params);
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
@@ -1090,8 +1375,8 @@ class QueryBuilder extends \yii\base\BaseObject
}
$orders = [];
foreach ($columns as $name => $direction) {
- if ($direction instanceof Expression) {
- $orders[] = $direction->expression;
+ if ($direction instanceof ExpressionInterface) {
+ $orders[] = $this->buildExpression($direction);
$params = array_merge($params, $direction->params);
} else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
@@ -1126,7 +1411,7 @@ class QueryBuilder extends \yii\base\BaseObject
*/
protected function hasLimit($limit)
{
- return ($limit instanceof Expression) || ctype_digit((string) $limit);
+ return ($limit instanceof ExpressionInterface) || ctype_digit((string) $limit);
}
/**
@@ -1136,7 +1421,7 @@ class QueryBuilder extends \yii\base\BaseObject
*/
protected function hasOffset($offset)
{
- return ($offset instanceof Expression) || ctype_digit((string) $offset) && (string) $offset !== '0';
+ return ($offset instanceof ExpressionInterface) || ctype_digit((string) $offset) && (string) $offset !== '0';
}
/**
@@ -1177,53 +1462,72 @@ class QueryBuilder extends \yii\base\BaseObject
return $columns;
}
+ $rawColumns = $columns;
$columns = preg_split('/\s*,\s*/', $columns, -1, PREG_SPLIT_NO_EMPTY);
+ if ($columns === false) {
+ throw new InvalidArgumentException("$rawColumns is not valid columns.");
+ }
}
foreach ($columns as $i => $column) {
- if ($column instanceof Expression) {
- $columns[$i] = $column->expression;
+ if ($column instanceof ExpressionInterface) {
+ $columns[$i] = $this->buildExpression($column);
} elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column);
}
}
- return is_array($columns) ? implode(', ', $columns) : $columns;
+ return implode(', ', $columns);
}
/**
* Parses the condition specification and generates the corresponding SQL expression.
- * @param string|array|Expression $condition the condition specification. Please refer to [[Query::where()]]
+ * @param string|array|ExpressionInterface $condition the condition specification. Please refer to [[Query::where()]]
* on how to specify a condition.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
*/
public function buildCondition($condition, &$params)
{
- if ($condition instanceof Expression) {
- foreach ($condition->params as $n => $v) {
- $params[$n] = $v;
+ if (is_array($condition)) {
+ if (empty($condition)) {
+ return '';
}
- return $condition->expression;
- } elseif (!is_array($condition)) {
- return (string) $condition;
- } elseif (empty($condition)) {
- return '';
+ $condition = $this->createConditionFromArray($condition);
+ }
+
+ if ($condition instanceof ExpressionInterface) {
+ return $this->buildExpression($condition, $params);
}
+ return (string) $condition;
+ }
+
+ /**
+ * Transforms $condition defined in array format (as described in [[Query::where()]]
+ * to instance of [[yii\db\condition\ConditionInterface|ConditionInterface]] according to
+ * [[conditionClasses]] map.
+ *
+ * @param string|array $condition
+ * @see conditionClasses
+ * @return ConditionInterface
+ * @since 2.0.14
+ */
+ public function createConditionFromArray($condition)
+ {
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
- $operator = strtoupper($condition[0]);
- if (isset($this->conditionBuilders[$operator])) {
- $method = $this->conditionBuilders[$operator];
+ $operator = strtoupper(array_shift($condition));
+ if (isset($this->conditionClasses[$operator])) {
+ $className = $this->conditionClasses[$operator];
} else {
- $method = 'buildSimpleCondition';
+ $className = 'yii\db\conditions\SimpleCondition';
}
- array_shift($condition);
- return $this->$method($operator, $condition, $params);
+ /** @var ConditionInterface $className */
+ return $className::fromArrayDefinition($operator, $condition);
}
// hash format: 'column1' => 'value1', 'column2' => 'value2', ...
- return $this->buildHashCondition($condition, $params);
+ return new HashCondition($condition);
}
/**
@@ -1231,34 +1535,11 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $condition the condition specification.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildHashCondition($condition, &$params)
{
- $parts = [];
- foreach ($condition as $column => $value) {
- if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
- // IN condition
- $parts[] = $this->buildInCondition('IN', [$column, $value], $params);
- } else {
- if (strpos($column, '(') === false) {
- $column = $this->db->quoteColumnName($column);
- }
- if ($value === null) {
- $parts[] = "$column IS NULL";
- } elseif ($value instanceof Expression) {
- $parts[] = "$column=" . $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
- } else {
- $phName = self::PARAM_PREFIX . count($params);
- $parts[] = "$column=$phName";
- $params[$phName] = $value;
- }
- }
- }
-
- return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
+ return $this->buildCondition(new HashCondition($condition), $params);
}
/**
@@ -1267,29 +1548,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $operands the SQL expressions to connect.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildAndCondition($operator, $operands, &$params)
{
- $parts = [];
- foreach ($operands as $operand) {
- if (is_array($operand)) {
- $operand = $this->buildCondition($operand, $params);
- }
- if ($operand instanceof Expression) {
- foreach ($operand->params as $n => $v) {
- $params[$n] = $v;
- }
- $operand = $operand->expression;
- }
- if ($operand !== '') {
- $parts[] = $operand;
- }
- }
- if (!empty($parts)) {
- return '(' . implode(") $operator (", $parts) . ')';
- }
-
- return '';
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1299,22 +1563,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidArgumentException if wrong number of operands have been given.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildNotCondition($operator, $operands, &$params)
{
- if (count($operands) !== 1) {
- throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
- }
-
- $operand = reset($operands);
- if (is_array($operand) || $operand instanceof Expression) {
- $operand = $this->buildCondition($operand, $params);
- }
- if ($operand === '') {
- return '';
- }
-
- return "$operator ($operand)";
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1325,38 +1579,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidArgumentException if wrong number of operands have been given.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildBetweenCondition($operator, $operands, &$params)
{
- if (!isset($operands[0], $operands[1], $operands[2])) {
- throw new InvalidArgumentException("Operator '$operator' requires three operands.");
- }
-
- [$column, $value1, $value2] = $operands;
-
- if (strpos($column, '(') === false) {
- $column = $this->db->quoteColumnName($column);
- }
- if ($value1 instanceof Expression) {
- foreach ($value1->params as $n => $v) {
- $params[$n] = $v;
- }
- $phName1 = $value1->expression;
- } else {
- $phName1 = self::PARAM_PREFIX . count($params);
- $params[$phName1] = $value1;
- }
- if ($value2 instanceof Expression) {
- foreach ($value2->params as $n => $v) {
- $params[$n] = $v;
- }
- $phName2 = $value2->expression;
- } else {
- $phName2 = self::PARAM_PREFIX . count($params);
- $params[$phName2] = $value2;
- }
-
- return "$column $operator $phName1 AND $phName2";
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1370,134 +1598,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws Exception if wrong number of operands have been given.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildInCondition($operator, $operands, &$params)
{
- if (!isset($operands[0], $operands[1])) {
- throw new Exception("Operator '$operator' requires two operands.");
- }
-
- [$column, $values] = $operands;
-
- if ($column === []) {
- // no columns to test against
- return $operator === 'IN' ? '0=1' : '';
- }
-
- if ($values instanceof Query) {
- return $this->buildSubqueryInCondition($operator, $column, $values, $params);
- }
- if (!is_array($values) && !$values instanceof \Traversable) {
- // ensure values is an array
- $values = (array) $values;
- }
-
- if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
- return $this->buildCompositeInCondition($operator, $column, $values, $params);
- } elseif (is_array($column)) {
- $column = reset($column);
- }
-
- $sqlValues = [];
- foreach ($values as $i => $value) {
- if (is_array($value) || $value instanceof \ArrayAccess) {
- $value = isset($value[$column]) ? $value[$column] : null;
- }
- if ($value === null) {
- $sqlValues[$i] = 'NULL';
- } elseif ($value instanceof Expression) {
- $sqlValues[$i] = $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
- } else {
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = $value;
- $sqlValues[$i] = $phName;
- }
- }
-
- if (empty($sqlValues)) {
- return $operator === 'IN' ? '0=1' : '';
- }
-
- if (strpos($column, '(') === false) {
- $column = $this->db->quoteColumnName($column);
- }
-
- if (count($sqlValues) > 1) {
- return "$column $operator (" . implode(', ', $sqlValues) . ')';
- }
-
- $operator = $operator === 'IN' ? '=' : '<>';
- return $column . $operator . reset($sqlValues);
- }
-
- /**
- * Builds SQL for IN condition.
- *
- * @param string $operator
- * @param array $columns
- * @param Query $values
- * @param array $params
- * @return string SQL
- */
- protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
- {
- [$sql, $params] = $this->build($values, $params);
- if (is_array($columns)) {
- foreach ($columns as $i => $col) {
- if (strpos($col, '(') === false) {
- $columns[$i] = $this->db->quoteColumnName($col);
- }
- }
-
- return '(' . implode(', ', $columns) . ") $operator ($sql)";
- }
-
- if (strpos($columns, '(') === false) {
- $columns = $this->db->quoteColumnName($columns);
- }
-
- return "$columns $operator ($sql)";
- }
-
- /**
- * Builds SQL for IN condition.
- *
- * @param string $operator
- * @param array|\Traversable $columns
- * @param array $values
- * @param array $params
- * @return string SQL
- */
- protected function buildCompositeInCondition($operator, $columns, $values, &$params)
- {
- $vss = [];
- foreach ($values as $value) {
- $vs = [];
- foreach ($columns as $column) {
- if (isset($value[$column])) {
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = $value[$column];
- $vs[] = $phName;
- } else {
- $vs[] = 'NULL';
- }
- }
- $vss[] = '(' . implode(', ', $vs) . ')';
- }
-
- if (empty($vss)) {
- return $operator === 'IN' ? '0=1' : '';
- }
-
- $sqlColumns = [];
- foreach ($columns as $i => $column) {
- $sqlColumns[] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
- }
-
- return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1519,56 +1625,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidArgumentException if wrong number of operands have been given.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildLikeCondition($operator, $operands, &$params)
{
- if (!isset($operands[0], $operands[1])) {
- throw new InvalidArgumentException("Operator '$operator' requires two operands.");
- }
-
- $escape = isset($operands[2]) ? $operands[2] : $this->likeEscapingReplacements;
- unset($operands[2]);
-
- if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
- throw new InvalidArgumentException("Invalid operator '$operator'.");
- }
- $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
- $not = !empty($matches[3]);
- $operator = $matches[2];
-
- [$column, $values] = $operands;
-
- if (!is_array($values)) {
- $values = [$values];
- }
-
- if (empty($values)) {
- return $not ? '' : '0=1';
- }
-
- if (strpos($column, '(') === false) {
- $column = $this->db->quoteColumnName($column);
- }
-
- $parts = [];
- foreach ($values as $value) {
- if ($value instanceof Expression) {
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
- $phName = $value->expression;
- } else {
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%');
- }
- $escapeSql = '';
- if ($this->likeEscapeCharacter !== null) {
- $escapeSql = " ESCAPE '{$this->likeEscapeCharacter}'";
- }
- $parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
- }
-
- return implode($andor, $parts);
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1578,15 +1640,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidArgumentException if the operand is not a [[Query]] object.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildExistsCondition($operator, $operands, &$params)
{
- if ($operands[0] instanceof Query) {
- [$sql, $params] = $this->build($operands[0], $params);
- return "$operator ($sql)";
- }
-
- throw new InvalidArgumentException('Subquery for EXISTS operator must be a Query object.');
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1596,35 +1655,12 @@ class QueryBuilder extends \yii\base\BaseObject
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
* @throws InvalidArgumentException if wrong number of operands have been given.
+ * @deprecated since 2.0.14. Use `buildCondition()` instead.
*/
public function buildSimpleCondition($operator, $operands, &$params)
{
- if (count($operands) !== 2) {
- throw new InvalidArgumentException("Operator '$operator' requires two operands.");
- }
-
- [$column, $value] = $operands;
-
- if (strpos($column, '(') === false) {
- $column = $this->db->quoteColumnName($column);
- }
-
- if ($value === null) {
- return "$column $operator NULL";
- } elseif ($value instanceof Expression) {
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
-
- return "$column $operator {$value->expression}";
- } elseif ($value instanceof Query) {
- [$sql, $params] = $this->build($value, $params);
- return "$column $operator ($sql)";
- }
-
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = $value;
- return "$column $operator $phName";
+ array_unshift($operands, $operator);
+ return $this->buildCondition($operands, $params);
}
/**
@@ -1637,4 +1673,21 @@ class QueryBuilder extends \yii\base\BaseObject
{
return 'SELECT EXISTS(' . $rawSql . ')';
}
+
+ /**
+ * Helper method to add $value to $params array using [[PARAM_PREFIX]].
+ *
+ * @param string|null $value
+ * @param array $params passed by reference
+ * @return string the placeholder name in $params array
+ *
+ * @since 2.0.14
+ */
+ public function bindParam($value, &$params)
+ {
+ $phName = self::PARAM_PREFIX . count($params);
+ $params[$phName] = $value;
+
+ return $phName;
+ }
}
diff --git a/framework/db/QueryExpressionBuilder.php b/framework/db/QueryExpressionBuilder.php
new file mode 100644
index 0000000..3186d2b
--- /dev/null
+++ b/framework/db/QueryExpressionBuilder.php
@@ -0,0 +1,36 @@
+
+ * @since 2.0.14
+ */
+class QueryExpressionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|Query $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ list($sql, $params) = $this->queryBuilder->build($expression, $params);
+
+ return "($sql)";
+ }
+}
diff --git a/framework/db/QueryTrait.php b/framework/db/QueryTrait.php
index 2052196..e9dc68e 100644
--- a/framework/db/QueryTrait.php
+++ b/framework/db/QueryTrait.php
@@ -27,13 +27,13 @@ trait QueryTrait
*/
public $where;
/**
- * @var int|Expression maximum number of records to be returned. May be an instance of [[Expression]].
+ * @var int|ExpressionInterface maximum number of records to be returned. May be an instance of [[ExpressionInterface]].
* If not set or less than 0, it means no limit.
*/
public $limit;
/**
- * @var int|Expression zero-based offset from where the records are to be returned.
- * May be an instance of [[Expression]]. If not set or less than 0, it means starting from the beginning.
+ * @var int|ExpressionInterface zero-based offset from where the records are to be returned.
+ * May be an instance of [[ExpressionInterface]]. If not set or less than 0, it means starting from the beginning.
*/
public $offset;
/**
@@ -41,7 +41,7 @@ trait QueryTrait
* The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which
* can be either [SORT_ASC](http://php.net/manual/en/array.constants.php#constant.sort-asc)
* or [SORT_DESC](http://php.net/manual/en/array.constants.php#constant.sort-desc).
- * The array may also contain [[Expression]] objects. If that is the case, the expressions
+ * The array may also contain [[ExpressionInterface]] objects. If that is the case, the expressions
* will be converted into strings without any change.
*/
public $orderBy;
@@ -305,7 +305,7 @@ trait QueryTrait
/**
* Sets the ORDER BY part of the query.
- * @param string|array|Expression $columns the columns (and the directions) to be ordered by.
+ * @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. `"id ASC, name DESC"`) or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
*
@@ -316,7 +316,7 @@ trait QueryTrait
* to represent the order-by information. Otherwise, the method will not be able to correctly determine
* the order-by columns.
*
- * Since version 2.0.7, an [[Expression]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
+ * Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* @return $this the query object itself
* @see addOrderBy()
*/
@@ -328,7 +328,7 @@ trait QueryTrait
/**
* Adds additional ORDER BY columns to the query.
- * @param string|array|Expression $columns the columns (and the directions) to be ordered by.
+ * @param string|array|ExpressionInterface $columns the columns (and the directions) to be ordered by.
* Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array
* (e.g. `['id' => SORT_ASC, 'name' => SORT_DESC]`).
*
@@ -339,7 +339,7 @@ trait QueryTrait
* to represent the order-by information. Otherwise, the method will not be able to correctly determine
* the order-by columns.
*
- * Since version 2.0.7, an [[Expression]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
+ * Since version 2.0.7, an [[ExpressionInterface]] object can be passed to specify the ORDER BY part explicitly in plain SQL.
* @return $this the query object itself
* @see orderBy()
*/
@@ -358,12 +358,12 @@ trait QueryTrait
/**
* Normalizes format of ORDER BY data.
*
- * @param array|string|Expression $columns the columns value to normalize. See [[orderBy]] and [[addOrderBy]].
+ * @param array|string|ExpressionInterface $columns the columns value to normalize. See [[orderBy]] and [[addOrderBy]].
* @return array
*/
protected function normalizeOrderBy($columns)
{
- if ($columns instanceof Expression) {
+ if ($columns instanceof ExpressionInterface) {
return [$columns];
} elseif (is_array($columns)) {
return $columns;
@@ -384,7 +384,7 @@ trait QueryTrait
/**
* Sets the LIMIT part of the query.
- * @param int|Expression|null $limit the limit. Use null or negative value to disable limit.
+ * @param int|ExpressionInterface|null $limit the limit. Use null or negative value to disable limit.
* @return $this the query object itself
*/
public function limit($limit)
@@ -395,7 +395,7 @@ trait QueryTrait
/**
* Sets the OFFSET part of the query.
- * @param int|Expression|null $offset the offset. Use null or negative value to disable offset.
+ * @param int|ExpressionInterface|null $offset the offset. Use null or negative value to disable offset.
* @return $this the query object itself
*/
public function offset($offset)
diff --git a/framework/db/Schema.php b/framework/db/Schema.php
index 58a969a..c0258c5 100644
--- a/framework/db/Schema.php
+++ b/framework/db/Schema.php
@@ -15,6 +15,7 @@ use yii\base\NotSupportedException;
use yii\caching\Cache;
use yii\caching\CacheInterface;
use yii\caching\TagDependency;
+use yii\helpers\StringHelper;
/**
* Schema is the base class for concrete DBMS-specific schema classes.
@@ -26,6 +27,7 @@ use yii\caching\TagDependency;
* @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
* @property string[] $schemaNames All schema names in the database, except system schemas. This property is
* read-only.
+ * @property string $serverVersion Server version as a string. This property is read-only.
* @property string[] $tableNames All table names in the database. This property is read-only.
* @property TableSchema[] $tableSchemas The metadata for all tables in the database. Each array element is an
* instance of [[TableSchema]] or its child class. This property is read-only.
@@ -48,6 +50,7 @@ abstract class Schema extends BaseObject
const TYPE_CHAR = 'char';
const TYPE_STRING = 'string';
const TYPE_TEXT = 'text';
+ const TYPE_TINYINT = 'tinyint';
const TYPE_SMALLINT = 'smallint';
const TYPE_INTEGER = 'integer';
const TYPE_BIGINT = 'bigint';
@@ -61,6 +64,7 @@ abstract class Schema extends BaseObject
const TYPE_BINARY = 'binary';
const TYPE_BOOLEAN = 'boolean';
const TYPE_MONEY = 'money';
+ const TYPE_JSON = 'json';
/**
* Schema cache version, to detect incompatibilities in cached values when the
* data format of the cache changes.
@@ -89,6 +93,19 @@ abstract class Schema extends BaseObject
public $columnSchemaClass = ColumnSchema::class;
/**
+ * @var string|string[] character used to quote schema, table, etc. names.
+ * An array of 2 characters can be used in case starting and ending characters are different.
+ * @since 2.0.14
+ */
+ protected $tableQuoteCharacter = "'";
+ /**
+ * @var string|string[] character used to quote column names.
+ * An array of 2 characters can be used in case starting and ending characters are different.
+ * @since 2.0.14
+ */
+ protected $columnQuoteCharacter = '"';
+
+ /**
* @var array list of ALL schema names in the database, except system schemas
*/
private $_schemaNames;
@@ -104,6 +121,10 @@ abstract class Schema extends BaseObject
* @var QueryBuilder the query builder for this database
*/
private $_builder;
+ /**
+ * @var string server version as a string.
+ */
+ private $_serverVersion;
/**
@@ -508,7 +529,12 @@ abstract class Schema extends BaseObject
*/
public function quoteSimpleTableName($name)
{
- return strpos($name, "'") !== false ? $name : "'" . $name . "'";
+ if (is_string($this->tableQuoteCharacter)) {
+ $startingCharacter = $endingCharacter = $this->tableQuoteCharacter;
+ } else {
+ list($startingCharacter, $endingCharacter) = $this->tableQuoteCharacter;
+ }
+ return strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
}
/**
@@ -520,7 +546,48 @@ abstract class Schema extends BaseObject
*/
public function quoteSimpleColumnName($name)
{
- return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"';
+ if (is_string($this->tableQuoteCharacter)) {
+ $startingCharacter = $endingCharacter = $this->columnQuoteCharacter;
+ } else {
+ list($startingCharacter, $endingCharacter) = $this->columnQuoteCharacter;
+ }
+ return $name === '*' || strpos($name, $startingCharacter) !== false ? $name : $startingCharacter . $name . $endingCharacter;
+ }
+
+ /**
+ * Unquotes a simple table name.
+ * A simple table name should contain the table name only without any schema prefix.
+ * If the table name is not quoted, this method will do nothing.
+ * @param string $name table name.
+ * @return string unquoted table name.
+ * @since 2.0.14
+ */
+ public function unquoteSimpleTableName($name)
+ {
+ if (is_string($this->tableQuoteCharacter)) {
+ $startingCharacter = $this->tableQuoteCharacter;
+ } else {
+ $startingCharacter = $this->tableQuoteCharacter[0];
+ }
+ return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
+ }
+
+ /**
+ * Unquotes a simple column name.
+ * A simple column name should contain the column name only without any prefix.
+ * If the column name is not quoted or is the asterisk character '*', this method will do nothing.
+ * @param string $name column name.
+ * @return string unquoted column name.
+ * @since 2.0.14
+ */
+ public function unquoteSimpleColumnName($name)
+ {
+ if (is_string($this->columnQuoteCharacter)) {
+ $startingCharacter = $this->columnQuoteCharacter;
+ } else {
+ $startingCharacter = $this->columnQuoteCharacter[0];
+ }
+ return strpos($name, $startingCharacter) === false ? $name : substr($name, 1, -1);
}
/**
@@ -550,13 +617,15 @@ abstract class Schema extends BaseObject
{
static $typeMap = [
// abstract type => php type
- 'smallint' => 'integer',
- 'integer' => 'integer',
- 'bigint' => 'integer',
- 'boolean' => 'boolean',
- 'float' => 'double',
- 'double' => 'double',
- 'binary' => 'resource',
+ self::TYPE_TINYINT => 'integer',
+ self::TYPE_SMALLINT => 'integer',
+ self::TYPE_INTEGER => 'integer',
+ self::TYPE_BIGINT => 'integer',
+ self::TYPE_BOOLEAN => 'boolean',
+ self::TYPE_FLOAT => 'double',
+ self::TYPE_DOUBLE => 'double',
+ self::TYPE_BINARY => 'resource',
+ self::TYPE_JSON => 'array',
];
if (isset($typeMap[$column->type])) {
if ($column->type === 'bigint') {
@@ -607,6 +676,19 @@ abstract class Schema extends BaseObject
}
/**
+ * Returns a server version as a string comparable by [[\version_compare()]].
+ * @return string server version as a string.
+ * @since 2.0.14
+ */
+ public function getServerVersion()
+ {
+ if ($this->_serverVersion === null) {
+ $this->_serverVersion = $this->db->getSlavePdo()->getAttribute(\PDO::ATTR_SERVER_VERSION);
+ }
+ return $this->_serverVersion;
+ }
+
+ /**
* Returns the cache key for the specified table name.
* @param string $name the table name.
* @return mixed the cache key.
diff --git a/framework/db/SchemaBuilderTrait.php b/framework/db/SchemaBuilderTrait.php
index 562f648..e57c4b0 100644
--- a/framework/db/SchemaBuilderTrait.php
+++ b/framework/db/SchemaBuilderTrait.php
@@ -98,6 +98,18 @@ trait SchemaBuilderTrait
}
/**
+ * Creates a tinyint column. If tinyint is not supported by the DBMS, smallint will be used.
+ * @param int $length column size or precision definition.
+ * This parameter will be ignored if not supported by the DBMS.
+ * @return ColumnSchemaBuilder the column instance which can be further customized.
+ * @since 2.0.14
+ */
+ public function tinyInteger($length = null)
+ {
+ return $this->getDb()->getSchema()->createColumnSchemaBuilder(Schema::TYPE_TINYINT, $length);
+ }
+
+ /**
* Creates a smallint column.
* @param int $length column size or precision definition.
* This parameter will be ignored if not supported by the DBMS.
@@ -272,4 +284,25 @@ trait SchemaBuilderTrait
return $this->getDb()->getSchema()->createColumnSchemaBuilder(Schema::TYPE_MONEY, $length);
}
+
+ /**
+ * Creates a JSON column.
+ * @return ColumnSchemaBuilder the column instance which can be further customized.
+ * @since 2.0.14
+ * @throws \yii\base\Exception
+ */
+ public function json()
+ {
+ /*
+ * TODO Remove in Yii 2.1
+ *
+ * Disabled due to bug in MySQL extension
+ * @link https://bugs.php.net/bug.php?id=70384
+ */
+ if (version_compare(PHP_VERSION, '5.6', '<') && $this->getDb()->getDriverName() === 'mysql') {
+ throw new \yii\base\Exception('JSON column type is not supported in PHP < 5.6');
+ }
+
+ return $this->getDb()->getSchema()->createColumnSchemaBuilder(Schema::TYPE_JSON);
+ }
}
diff --git a/framework/db/TableSchema.php b/framework/db/TableSchema.php
index 2e217ed..33e2c12 100644
--- a/framework/db/TableSchema.php
+++ b/framework/db/TableSchema.php
@@ -7,8 +7,8 @@
namespace yii\db;
-use yii\base\InvalidArgumentException;
use yii\base\BaseObject;
+use yii\base\InvalidArgumentException;
/**
* TableSchema represents the metadata of a database table.
diff --git a/framework/db/conditions/AndCondition.php b/framework/db/conditions/AndCondition.php
new file mode 100644
index 0000000..32382ed
--- /dev/null
+++ b/framework/db/conditions/AndCondition.php
@@ -0,0 +1,27 @@
+
+ * @since 2.0.14
+ */
+class AndCondition extends ConjunctionCondition
+{
+ /**
+ * Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
+ *
+ * @return string
+ */
+ public function getOperator()
+ {
+ return 'AND';
+ }
+}
diff --git a/framework/db/conditions/BetweenColumnsCondition.php b/framework/db/conditions/BetweenColumnsCondition.php
new file mode 100644
index 0000000..e5fb731
--- /dev/null
+++ b/framework/db/conditions/BetweenColumnsCondition.php
@@ -0,0 +1,121 @@
+select('time')->from('log')->orderBy('id ASC')->limit(1),
+ * 'update_time'
+ * );
+ *
+ * // Will be built to:
+ * // NOW() BETWEEN (SELECT time FROM log ORDER BY id ASC LIMIT 1) AND update_time
+ * ```
+ *
+ * @author Dmytro Naumenko
+ * @since 2.0.14
+ */
+class BetweenColumnsCondition implements ConditionInterface
+{
+ /**
+ * @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
+ */
+ private $operator;
+ /**
+ * @var mixed the value to compare against
+ */
+ private $value;
+ /**
+ * @var string|ExpressionInterface|Query the column name or expression that is a beginning of the interval
+ */
+ private $intervalStartColumn;
+ /**
+ * @var string|ExpressionInterface|Query the column name or expression that is an end of the interval
+ */
+ private $intervalEndColumn;
+
+
+ /**
+ * Creates a condition with the `BETWEEN` operator.
+ *
+ * @param mixed the value to compare against
+ * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
+ * @param string|ExpressionInterface $intervalStartColumn the column name or expression that is a beginning of the interval
+ * @param string|ExpressionInterface $intervalEndColumn the column name or expression that is an end of the interval
+ */
+ public function __construct($value, $operator, $intervalStartColumn, $intervalEndColumn)
+ {
+ $this->value = $value;
+ $this->operator = $operator;
+ $this->intervalStartColumn = $intervalStartColumn;
+ $this->intervalEndColumn = $intervalEndColumn;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @return string|ExpressionInterface|Query
+ */
+ public function getIntervalStartColumn()
+ {
+ return $this->intervalStartColumn;
+ }
+
+ /**
+ * @return string|ExpressionInterface|Query
+ */
+ public function getIntervalEndColumn()
+ {
+ return $this->intervalEndColumn;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1], $operands[2])) {
+ throw new InvalidArgumentException("Operator '$operator' requires three operands.");
+ }
+
+ return new static($operands[0], $operator, $operands[1], $operands[2]);
+ }
+}
diff --git a/framework/db/conditions/BetweenColumnsConditionBuilder.php b/framework/db/conditions/BetweenColumnsConditionBuilder.php
new file mode 100644
index 0000000..8a55bf4
--- /dev/null
+++ b/framework/db/conditions/BetweenColumnsConditionBuilder.php
@@ -0,0 +1,81 @@
+
+ * @since 2.0.14
+ */
+class BetweenColumnsConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|BetweenColumnsCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+
+ $startColumn = $this->escapeColumnName($expression->getIntervalStartColumn(), $params);
+ $endColumn = $this->escapeColumnName($expression->getIntervalEndColumn(), $params);
+ $value = $this->createPlaceholder($expression->getValue(), $params);
+
+ return "$value $operator $startColumn AND $endColumn";
+ }
+
+ /**
+ * Prepares column name to be used in SQL statement.
+ *
+ * @param Query|ExpressionInterface|string $columnName
+ * @param array $params the binding parameters.
+ * @return string
+ */
+ protected function escapeColumnName($columnName, &$params = [])
+ {
+ if ($columnName instanceof Query) {
+ list($sql, $params) = $this->queryBuilder->build($columnName, $params);
+ return "($sql)";
+ } elseif ($columnName instanceof ExpressionInterface) {
+ return $this->queryBuilder->buildExpression($columnName, $params);
+ } elseif (strpos($columnName, '(') === false) {
+ return $this->queryBuilder->db->quoteColumnName($columnName);
+ }
+
+ return $columnName;
+ }
+
+ /**
+ * Attaches $value to $params array and returns placeholder.
+ *
+ * @param mixed $value
+ * @param array $params passed by reference
+ * @return string
+ */
+ protected function createPlaceholder($value, &$params)
+ {
+ if ($value instanceof ExpressionInterface) {
+ return $this->queryBuilder->buildExpression($value, $params);
+ }
+
+ return $this->queryBuilder->bindParam($value, $params);
+ }
+}
diff --git a/framework/db/conditions/BetweenCondition.php b/framework/db/conditions/BetweenCondition.php
new file mode 100644
index 0000000..159b1a6
--- /dev/null
+++ b/framework/db/conditions/BetweenCondition.php
@@ -0,0 +1,98 @@
+
+ * @since 2.0.14
+ */
+class BetweenCondition implements ConditionInterface
+{
+ /**
+ * @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
+ */
+ private $operator;
+ /**
+ * @var mixed the column name to the left of [[operator]]
+ */
+ private $column;
+ /**
+ * @var mixed beginning of the interval
+ */
+ private $intervalStart;
+ /**
+ * @var mixed end of the interval
+ */
+ private $intervalEnd;
+
+
+ /**
+ * Creates a condition with the `BETWEEN` operator.
+ *
+ * @param mixed $column the literal to the left of $operator
+ * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
+ * @param mixed $intervalStart beginning of the interval
+ * @param mixed $intervalEnd end of the interval
+ */
+ public function __construct($column, $operator, $intervalStart, $intervalEnd)
+ {
+ $this->column = $column;
+ $this->operator = $operator;
+ $this->intervalStart = $intervalStart;
+ $this->intervalEnd = $intervalEnd;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getColumn()
+ {
+ return $this->column;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getIntervalStart()
+ {
+ return $this->intervalStart;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getIntervalEnd()
+ {
+ return $this->intervalEnd;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1], $operands[2])) {
+ throw new InvalidArgumentException("Operator '$operator' requires three operands.");
+ }
+
+ return new static($operands[0], $operator, $operands[1], $operands[2]);
+ }
+}
diff --git a/framework/db/conditions/BetweenConditionBuilder.php b/framework/db/conditions/BetweenConditionBuilder.php
new file mode 100644
index 0000000..5247319
--- /dev/null
+++ b/framework/db/conditions/BetweenConditionBuilder.php
@@ -0,0 +1,63 @@
+
+ * @since 2.0.14
+ */
+class BetweenConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|BetweenCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+ $column = $expression->getColumn();
+
+ if (strpos($column, '(') === false) {
+ $column = $this->queryBuilder->db->quoteColumnName($column);
+ }
+
+ $phName1 = $this->createPlaceholder($expression->getIntervalStart(), $params);
+ $phName2 = $this->createPlaceholder($expression->getIntervalEnd(), $params);
+
+ return "$column $operator $phName1 AND $phName2";
+ }
+
+ /**
+ * Attaches $value to $params array and returns placeholder.
+ *
+ * @param mixed $value
+ * @param array $params passed by reference
+ * @return string
+ */
+ protected function createPlaceholder($value, &$params)
+ {
+ if ($value instanceof ExpressionInterface) {
+ return $this->queryBuilder->buildExpression($value, $params);
+ }
+
+ return $this->queryBuilder->bindParam($value, $params);
+ }
+}
diff --git a/framework/db/conditions/ConditionInterface.php b/framework/db/conditions/ConditionInterface.php
new file mode 100644
index 0000000..c3bbc2a
--- /dev/null
+++ b/framework/db/conditions/ConditionInterface.php
@@ -0,0 +1,33 @@
+
+ * @since 2.0.14
+ */
+interface ConditionInterface extends ExpressionInterface
+{
+ /**
+ * Creates object by array-definition as described in
+ * [Query Builder – Operator format](guide:db-query-builder#operator-format) guide article.
+ *
+ * @param string $operator operator in uppercase.
+ * @param array $operands array of corresponding operands
+ *
+ * @return $this
+ * @throws InvalidParamException if input parameters are not suitable for this condition
+ */
+ public static function fromArrayDefinition($operator, $operands);
+}
diff --git a/framework/db/conditions/ConjunctionCondition.php b/framework/db/conditions/ConjunctionCondition.php
new file mode 100644
index 0000000..985b8fc
--- /dev/null
+++ b/framework/db/conditions/ConjunctionCondition.php
@@ -0,0 +1,53 @@
+
+ * @since 2.0.14
+ */
+abstract class ConjunctionCondition implements ConditionInterface
+{
+ /**
+ * @var mixed[]
+ */
+ protected $expressions;
+
+
+ /**
+ * @param mixed $expressions
+ */
+ public function __construct($expressions) // TODO: use variadic params when PHP>5.6
+ {
+ $this->expressions = $expressions;
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function getExpressions()
+ {
+ return $this->expressions;
+ }
+
+ /**
+ * Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
+ * @return string
+ */
+ abstract public function getOperator();
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ return new static($operands);
+ }
+}
diff --git a/framework/db/conditions/ConjunctionConditionBuilder.php b/framework/db/conditions/ConjunctionConditionBuilder.php
new file mode 100644
index 0000000..00a9bce
--- /dev/null
+++ b/framework/db/conditions/ConjunctionConditionBuilder.php
@@ -0,0 +1,72 @@
+
+ * @since 2.0.14
+ */
+class ConjunctionConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|ConjunctionCondition $condition the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $condition, array &$params = [])
+ {
+ $parts = $this->buildExpressionsFrom($condition, $params);
+
+ if (empty($parts)) {
+ return '';
+ }
+
+ if (count($parts) === 1) {
+ return reset($parts);
+ }
+
+ return '(' . implode(") {$condition->getOperator()} (", $parts) . ')';
+ }
+
+ /**
+ * Builds expressions, that are stored in $condition
+ *
+ * @param ExpressionInterface|ConjunctionCondition $condition the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string[]
+ */
+ private function buildExpressionsFrom(ExpressionInterface $condition, &$params = [])
+ {
+ $parts = [];
+ foreach ($condition->getExpressions() as $condition) {
+ if (is_array($condition)) {
+ $condition = $this->queryBuilder->buildCondition($condition, $params);
+ }
+ if ($condition instanceof ExpressionInterface) {
+ $condition = $this->queryBuilder->buildExpression($condition, $params);
+ }
+ if ($condition !== '') {
+ $parts[] = $condition;
+ }
+ }
+
+ return $parts;
+ }
+}
diff --git a/framework/db/conditions/ExistsCondition.php b/framework/db/conditions/ExistsCondition.php
new file mode 100644
index 0000000..607db5f
--- /dev/null
+++ b/framework/db/conditions/ExistsCondition.php
@@ -0,0 +1,70 @@
+
+ * @since 2.0.14
+ */
+class ExistsCondition implements ConditionInterface
+{
+ /**
+ * @var string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
+ */
+ private $operator;
+ /**
+ * @var Query the [[Query]] object representing the sub-query.
+ */
+ private $query;
+
+
+ /**
+ * ExistsCondition constructor.
+ *
+ * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`)
+ * @param Query $query the [[Query]] object representing the sub-query.
+ */
+ public function __construct($operator, $query)
+ {
+ $this->operator = $operator;
+ $this->query = $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (!isset($operands[0]) || !$operands[0] instanceof Query) {
+ throw new InvalidArgumentException('Subquery for EXISTS operator must be a Query object.');
+ }
+
+ return new static($operator, $operands[0]);
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * @return Query
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+}
diff --git a/framework/db/conditions/ExistsConditionBuilder.php b/framework/db/conditions/ExistsConditionBuilder.php
new file mode 100644
index 0000000..e3ef3ea
--- /dev/null
+++ b/framework/db/conditions/ExistsConditionBuilder.php
@@ -0,0 +1,42 @@
+
+ * @since 2.0.14
+ */
+class ExistsConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|ExistsCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+ $query = $expression->getQuery();
+
+ $sql = $this->queryBuilder->buildExpression($query, $params);
+
+ return "$operator $sql";
+ }
+}
diff --git a/framework/db/conditions/HashCondition.php b/framework/db/conditions/HashCondition.php
new file mode 100644
index 0000000..7c25a5b
--- /dev/null
+++ b/framework/db/conditions/HashCondition.php
@@ -0,0 +1,49 @@
+
+ * @since 2.0.14
+ */
+class HashCondition implements ConditionInterface
+{
+ /**
+ * @var array|null the condition specification.
+ */
+ private $hash;
+
+
+ /**
+ * HashCondition constructor.
+ *
+ * @param array|null $hash
+ */
+ public function __construct($hash)
+ {
+ $this->hash = $hash;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getHash()
+ {
+ return $this->hash;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ return new static($operands);
+ }
+}
diff --git a/framework/db/conditions/HashConditionBuilder.php b/framework/db/conditions/HashConditionBuilder.php
new file mode 100644
index 0000000..5792841
--- /dev/null
+++ b/framework/db/conditions/HashConditionBuilder.php
@@ -0,0 +1,60 @@
+
+ * @since 2.0.14
+ */
+class HashConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|HashCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $hash = $expression->getHash();
+ $parts = [];
+ foreach ($hash as $column => $value) {
+ if (ArrayHelper::isTraversable($value) || $value instanceof Query) {
+ // IN condition
+ $parts[] = $this->queryBuilder->buildCondition(new InCondition($column, 'IN', $value), $params);
+ } else {
+ if (strpos($column, '(') === false) {
+ $column = $this->queryBuilder->db->quoteColumnName($column);
+ }
+ if ($value === null) {
+ $parts[] = "$column IS NULL";
+ } elseif ($value instanceof ExpressionInterface) {
+ $parts[] = "$column=" . $this->queryBuilder->buildExpression($value, $params);
+ } else {
+ $phName = $this->queryBuilder->bindParam($value, $params);
+ $parts[] = "$column=$phName";
+ }
+ }
+ }
+
+ return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')';
+ }
+}
diff --git a/framework/db/conditions/InCondition.php b/framework/db/conditions/InCondition.php
new file mode 100644
index 0000000..de30d37
--- /dev/null
+++ b/framework/db/conditions/InCondition.php
@@ -0,0 +1,89 @@
+
+ * @since 2.0.14
+ */
+class InCondition implements ConditionInterface
+{
+ /**
+ * @var string $operator the operator to use (e.g. `IN` or `NOT IN`)
+ */
+ private $operator;
+ /**
+ * @var string|string[] the column name. If it is an array, a composite `IN` condition
+ * will be generated.
+ */
+ private $column;
+ /**
+ * @var ExpressionInterface[]|string[]|int[] an array of values that [[column]] value should be among.
+ * If it is an empty array the generated expression will be a `false` value if
+ * [[operator]] is `IN` and empty if operator is `NOT IN`.
+ */
+ private $values;
+
+
+ /**
+ * SimpleCondition constructor
+ *
+ * @param string|string[] the column name. If it is an array, a composite `IN` condition
+ * will be generated.
+ * @param string $operator the operator to use (e.g. `IN` or `NOT IN`)
+ * @param array an array of values that [[column]] value should be among. If it is an empty array the generated
+ * expression will be a `false` value if [[operator]] is `IN` and empty if operator is `NOT IN`.
+ */
+ public function __construct($column, $operator, $values)
+ {
+ $this->column = $column;
+ $this->operator = $operator;
+ $this->values = $values;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getColumn()
+ {
+ return $this->column;
+ }
+
+ /**
+ * @return ExpressionInterface[]|string[]|int[]
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1])) {
+ throw new InvalidArgumentException("Operator '$operator' requires two operands.");
+ }
+
+ return new static($operands[0], $operator, $operands[1]);
+ }
+}
diff --git a/framework/db/conditions/InConditionBuilder.php b/framework/db/conditions/InConditionBuilder.php
new file mode 100644
index 0000000..87e4d8d
--- /dev/null
+++ b/framework/db/conditions/InConditionBuilder.php
@@ -0,0 +1,172 @@
+
+ * @since 2.0.14
+ */
+class InConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|InCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+ $column = $expression->getColumn();
+ $values = $expression->getValues();
+
+ if ($column === []) {
+ // no columns to test against
+ return $operator === 'IN' ? '0=1' : '';
+ }
+
+ if ($values instanceof Query) {
+ return $this->buildSubqueryInCondition($operator, $column, $values, $params);
+ }
+
+ if (!is_array($values) && !$values instanceof \Traversable) {
+ // ensure values is an array
+ $values = (array) $values;
+ }
+ if ($column instanceof \Traversable || ((is_array($column) || $column instanceof \Countable) && count($column) > 1)) {
+ return $this->buildCompositeInCondition($operator, $column, $values, $params);
+ }
+
+ if (is_array($column)) {
+ $column = reset($column);
+ }
+
+ $sqlValues = $this->buildValues($expression, $values, $params);
+ if (empty($sqlValues)) {
+ return $operator === 'IN' ? '0=1' : '';
+ }
+
+ if (strpos($column, '(') === false) {
+ $column = $this->queryBuilder->db->quoteColumnName($column);
+ }
+ if (count($sqlValues) > 1) {
+ return "$column $operator (" . implode(', ', $sqlValues) . ')';
+ }
+
+ $operator = $operator === 'IN' ? '=' : '<>';
+
+ return $column . $operator . reset($sqlValues);
+ }
+
+ /**
+ * Builds $values to be used in [[InCondition]]
+ *
+ * @param ConditionInterface|InCondition $condition
+ * @param array $values
+ * @param array $params the binding parameters
+ * @return array of prepared for SQL placeholders
+ */
+ protected function buildValues(ConditionInterface $condition, $values, &$params)
+ {
+ $sqlValues = [];
+ $column = $condition->getColumn();
+
+ foreach ($values as $i => $value) {
+ if (is_array($value) || $value instanceof \ArrayAccess) {
+ $value = isset($value[$column]) ? $value[$column] : null;
+ }
+ if ($value === null) {
+ $sqlValues[$i] = 'NULL';
+ } elseif ($value instanceof ExpressionInterface) {
+ $sqlValues[$i] = $this->queryBuilder->buildExpression($value, $params);
+ } else {
+ $sqlValues[$i] = $this->queryBuilder->bindParam($value, $params);
+ }
+ }
+
+ return $sqlValues;
+ }
+
+ /**
+ * Builds SQL for IN condition.
+ *
+ * @param string $operator
+ * @param array|string $columns
+ * @param Query $values
+ * @param array $params
+ * @return string SQL
+ */
+ protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
+ {
+ $sql = $this->queryBuilder->buildExpression($values, $params);
+
+ if (is_array($columns)) {
+ foreach ($columns as $i => $col) {
+ if (strpos($col, '(') === false) {
+ $columns[$i] = $this->queryBuilder->db->quoteColumnName($col);
+ }
+ }
+
+ return '(' . implode(', ', $columns) . ") $operator $sql";
+ }
+
+ if (strpos($columns, '(') === false) {
+ $columns = $this->queryBuilder->db->quoteColumnName($columns);
+ }
+
+ return "$columns $operator $sql";
+ }
+
+ /**
+ * Builds SQL for IN condition.
+ *
+ * @param string $operator
+ * @param array|\Traversable $columns
+ * @param array $values
+ * @param array $params
+ * @return string SQL
+ */
+ protected function buildCompositeInCondition($operator, $columns, $values, &$params)
+ {
+ $vss = [];
+ foreach ($values as $value) {
+ $vs = [];
+ foreach ($columns as $column) {
+ if (isset($value[$column])) {
+ $vs[] = $this->queryBuilder->bindParam($value[$column], $params);
+ } else {
+ $vs[] = 'NULL';
+ }
+ }
+ $vss[] = '(' . implode(', ', $vs) . ')';
+ }
+
+ if (empty($vss)) {
+ return $operator === 'IN' ? '0=1' : '';
+ }
+
+ $sqlColumns = [];
+ foreach ($columns as $i => $column) {
+ $sqlColumns[] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
+ }
+
+ return '(' . implode(', ', $sqlColumns) . ") $operator (" . implode(', ', $vss) . ')';
+ }
+}
diff --git a/framework/db/conditions/LikeCondition.php b/framework/db/conditions/LikeCondition.php
new file mode 100644
index 0000000..3586adf
--- /dev/null
+++ b/framework/db/conditions/LikeCondition.php
@@ -0,0 +1,78 @@
+
+ * @since 2.0.14
+ */
+class LikeCondition extends SimpleCondition
+{
+ /**
+ * @var array|false map of chars to their replacements, false if characters should not be escaped
+ * or either null or empty array if escaping is condition builder responsibility.
+ * By default it's set to `null`.
+ */
+ protected $escapingReplacements;
+
+
+ /**
+ * @param string $column the column name.
+ * @param string $operator the operator to use (e.g. `LIKE`, `NOT LIKE`, `OR LIKE` or `OR NOT LIKE`)
+ * @param string[]|string $value single value or an array of values that $column should be compared with.
+ * If it is an empty array the generated expression will be a `false` value if operator is `LIKE` or `OR LIKE`
+ * and empty if operator is `NOT LIKE` or `OR NOT LIKE`.
+ */
+ public function __construct($column, $operator, $value)
+ {
+ parent::__construct($column, $operator, $value);
+ }
+
+ /**
+ * This method allows to specify how to escape special characters in the value(s).
+ *
+ * @param array an array of mappings from the special characters to their escaped counterparts.
+ * You may use `false` or an empty array to indicate the values are already escaped and no escape
+ * should be applied. Note that when using an escape mapping (or the third operand is not provided),
+ * the values will be automatically enclosed within a pair of percentage characters.
+ */
+ public function setEscapingReplacements($escapingReplacements)
+ {
+ $this->escapingReplacements = $escapingReplacements;
+ }
+
+ /**
+ * @return array|false
+ */
+ public function getEscapingReplacements()
+ {
+ return $this->escapingReplacements;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1])) {
+ throw new InvalidArgumentException("Operator '$operator' requires two operands.");
+ }
+
+ $condition = new static($operands[0], $operator, $operands[1]);
+ if (isset($operands[2])) {
+ $condition->escapingReplacements = $operands[2];
+ }
+
+ return $condition;
+ }
+}
diff --git a/framework/db/conditions/LikeConditionBuilder.php b/framework/db/conditions/LikeConditionBuilder.php
new file mode 100644
index 0000000..62c3751
--- /dev/null
+++ b/framework/db/conditions/LikeConditionBuilder.php
@@ -0,0 +1,114 @@
+
+ * @since 2.0.14
+ */
+class LikeConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+ /**
+ * @var array map of chars to their replacements in LIKE conditions.
+ * By default it's configured to escape `%`, `_` and `\` with `\`.
+ */
+ protected $escapingReplacements = [
+ '%' => '\%',
+ '_' => '\_',
+ '\\' => '\\\\',
+ ];
+ /**
+ * @var string|null character used to escape special characters in LIKE conditions.
+ * By default it's assumed to be `\`.
+ */
+ protected $escapeCharacter;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|LikeCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+ $column = $expression->getColumn();
+ $values = $expression->getValue();
+ $escape = $expression->getEscapingReplacements();
+ if ($escape === null || $escape === []) {
+ $escape = $this->escapingReplacements;
+ }
+
+ list($andor, $not, $operator) = $this->parseOperator($operator);
+
+ if (!is_array($values)) {
+ $values = [$values];
+ }
+
+ if (empty($values)) {
+ return $not ? '' : '0=1';
+ }
+
+ if (strpos($column, '(') === false) {
+ $column = $this->queryBuilder->db->quoteColumnName($column);
+ }
+
+ $escapeSql = $this->getEscapeSql();
+ $parts = [];
+ foreach ($values as $value) {
+ if ($value instanceof ExpressionInterface) {
+ $phName = $this->queryBuilder->buildExpression($value, $params);
+ } else {
+ $phName = $this->queryBuilder->bindParam(empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'), $params);
+ }
+ $parts[] = "{$column} {$operator} {$phName}{$escapeSql}";
+ }
+
+ return implode($andor, $parts);
+ }
+
+ /**
+ * @return string
+ */
+ private function getEscapeSql()
+ {
+ if ($this->escapeCharacter !== null) {
+ return " ESCAPE '{$this->escapeCharacter}'";
+ }
+
+ return '';
+ }
+
+ /**
+ * @param string $operator
+ * @return array
+ */
+ protected function parseOperator($operator)
+ {
+ if (!preg_match('/^(AND |OR |)(((NOT |))I?LIKE)/', $operator, $matches)) {
+ throw new InvalidArgumentException("Invalid operator '$operator'.");
+ }
+ $andor = ' ' . (!empty($matches[1]) ? $matches[1] : 'AND ');
+ $not = !empty($matches[3]);
+ $operator = $matches[2];
+
+ return [$andor, $not, $operator];
+ }
+}
diff --git a/framework/db/conditions/NotCondition.php b/framework/db/conditions/NotCondition.php
new file mode 100644
index 0000000..f176620
--- /dev/null
+++ b/framework/db/conditions/NotCondition.php
@@ -0,0 +1,56 @@
+
+ * @since 2.0.14
+ */
+class NotCondition implements ConditionInterface
+{
+ /**
+ * @var mixed the condition to be negated
+ */
+ private $condition;
+
+
+ /**
+ * NotCondition constructor.
+ *
+ * @param mixed $condition the condition to be negated
+ */
+ public function __construct($condition)
+ {
+ $this->condition = $condition;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getCondition()
+ {
+ return $this->condition;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (count($operands) !== 1) {
+ throw new InvalidArgumentException("Operator '$operator' requires exactly one operand.");
+ }
+
+ return new static(array_shift($operands));
+ }
+}
diff --git a/framework/db/conditions/NotConditionBuilder.php b/framework/db/conditions/NotConditionBuilder.php
new file mode 100644
index 0000000..1023b7f
--- /dev/null
+++ b/framework/db/conditions/NotConditionBuilder.php
@@ -0,0 +1,51 @@
+
+ * @since 2.0.14
+ */
+class NotConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|NotCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operand = $expression->getCondition();
+ if ($operand === '') {
+ return '';
+ }
+
+ $expession = $this->queryBuilder->buildCondition($operand, $params);
+ return "{$this->getNegationOperator()} ($expession)";
+ }
+
+ /**
+ * @return string
+ */
+ protected function getNegationOperator()
+ {
+ return 'NOT';
+ }
+}
diff --git a/framework/db/conditions/OrCondition.php b/framework/db/conditions/OrCondition.php
new file mode 100644
index 0000000..f70d2da
--- /dev/null
+++ b/framework/db/conditions/OrCondition.php
@@ -0,0 +1,27 @@
+
+ * @since 2.0.14
+ */
+class OrCondition extends ConjunctionCondition
+{
+ /**
+ * Returns the operator that is represented by this condition class, e.g. `AND`, `OR`.
+ *
+ * @return string
+ */
+ public function getOperator()
+ {
+ return 'OR';
+ }
+}
diff --git a/framework/db/conditions/SimpleCondition.php b/framework/db/conditions/SimpleCondition.php
new file mode 100644
index 0000000..ba27d15
--- /dev/null
+++ b/framework/db/conditions/SimpleCondition.php
@@ -0,0 +1,84 @@
+
+ * @since 2.0.14
+ */
+class SimpleCondition implements ConditionInterface
+{
+ /**
+ * @var string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
+ */
+ private $operator;
+ /**
+ * @var mixed the column name to the left of [[operator]]
+ */
+ private $column;
+ /**
+ * @var mixed the value to the right of the [[operator]]
+ */
+ private $value;
+
+
+ /**
+ * SimpleCondition constructor
+ *
+ * @param mixed $column the literal to the left of $operator
+ * @param string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
+ * @param mixed $value the literal to the right of $operator
+ */
+ public function __construct($column, $operator, $value)
+ {
+ $this->column = $column;
+ $this->operator = $operator;
+ $this->value = $value;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOperator()
+ {
+ return $this->operator;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getColumn()
+ {
+ return $this->column;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ * @throws InvalidArgumentException if wrong number of operands have been given.
+ */
+ public static function fromArrayDefinition($operator, $operands)
+ {
+ if (count($operands) !== 2) {
+ throw new InvalidArgumentException("Operator '$operator' requires two operands.");
+ }
+
+ return new static($operands[0], $operator, $operands[1]);
+ }
+}
diff --git a/framework/db/conditions/SimpleConditionBuilder.php b/framework/db/conditions/SimpleConditionBuilder.php
new file mode 100644
index 0000000..512a164
--- /dev/null
+++ b/framework/db/conditions/SimpleConditionBuilder.php
@@ -0,0 +1,54 @@
+
+ * @since 2.0.14
+ */
+class SimpleConditionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * Method builds the raw SQL from the $expression that will not be additionally
+ * escaped or quoted.
+ *
+ * @param ExpressionInterface|SimpleCondition $expression the expression to be built.
+ * @param array $params the binding parameters.
+ * @return string the raw SQL that will not be additionally escaped or quoted.
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $operator = $expression->getOperator();
+ $column = $expression->getColumn();
+ $value = $expression->getValue();
+
+ if (strpos($column, '(') === false) {
+ $column = $this->queryBuilder->db->quoteColumnName($column);
+ }
+
+ if ($value === null) {
+ return "$column $operator NULL";
+ }
+ if ($value instanceof ExpressionInterface) {
+ return "$column $operator {$this->queryBuilder->buildExpression($value, $params)}";
+ }
+
+ $phName = $this->queryBuilder->bindParam($value, $params);
+ return "$column $operator $phName";
+ }
+}
diff --git a/framework/db/cubrid/QueryBuilder.php b/framework/db/cubrid/QueryBuilder.php
index e48f648..5281f8e 100644
--- a/framework/db/cubrid/QueryBuilder.php
+++ b/framework/db/cubrid/QueryBuilder.php
@@ -9,7 +9,9 @@ namespace yii\db\cubrid;
use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
+use yii\db\Constraint;
use yii\db\Exception;
+use yii\db\Expression;
/**
* QueryBuilder is the query builder for CUBRID databases (version 9.3.x and higher).
@@ -30,6 +32,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'char(1)',
Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'varchar',
+ Schema::TYPE_TINYINT => 'smallint',
Schema::TYPE_SMALLINT => 'smallint',
Schema::TYPE_INTEGER => 'int',
Schema::TYPE_BIGINT => 'bigint',
@@ -45,19 +48,72 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_MONEY => 'decimal(19,4)',
];
+
/**
* {@inheritdoc}
*/
- protected $likeEscapeCharacter = '!';
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\conditions\LikeCondition' => 'yii\db\cubrid\conditions\LikeConditionBuilder',
+ ]);
+ }
+
/**
* {@inheritdoc}
+ * @see https://www.cubrid.org/manual/en/9.3.0/sql/query/merge.html
*/
- protected $likeEscapingReplacements = [
- '%' => '!%',
- '_' => '!_',
- '!' => '!!',
- ];
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ /** @var Constraint[] $constraints */
+ list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
+ if (empty($uniqueNames)) {
+ return $this->insert($table, $insertColumns, $params);
+ }
+
+ $onCondition = ['or'];
+ $quotedTableName = $this->db->quoteTableName($table);
+ foreach ($constraints as $constraint) {
+ $constraintCondition = ['and'];
+ foreach ($constraint->columnNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
+ }
+ $onCondition[] = $constraintCondition;
+ }
+ $on = $this->buildCondition($onCondition, $params);
+ list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
+ $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
+ . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS "EXCLUDED" (' . implode(', ', $insertNames) . ') '
+ . 'ON ' . $on;
+ $insertValues = [];
+ foreach ($insertNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '"EXCLUDED".' . $quotedName;
+ }
+ $insertValues[] = $quotedName;
+ }
+ $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
+ . ' VALUES (' . implode(', ', $insertValues) . ')';
+ if ($updateColumns === false) {
+ return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
+ }
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '"EXCLUDED".' . $quotedName;
+ }
+ $updateColumns[$name] = new Expression($quotedName);
+ }
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ $updateSql = 'UPDATE SET ' . implode(', ', $updates);
+ return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
+ }
/**
* Creates a SQL statement for resetting the sequence value of a table's primary key.
@@ -120,7 +176,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @see http://www.cubrid.org/manual/93/en/sql/schema/table.html#drop-index-clause
*/
public function dropIndex($name, $table)
@@ -137,7 +193,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by CUBRID.
*/
public function addCheck($name, $table, $expression)
@@ -146,7 +202,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by CUBRID.
*/
public function dropCheck($name, $table)
diff --git a/framework/db/cubrid/Schema.php b/framework/db/cubrid/Schema.php
index cd63623..81ed900 100644
--- a/framework/db/cubrid/Schema.php
+++ b/framework/db/cubrid/Schema.php
@@ -9,6 +9,7 @@ namespace yii\db\cubrid;
use yii\base\NotSupportedException;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\Expression;
use yii\db\ForeignKeyConstraint;
@@ -23,7 +24,7 @@ use yii\helpers\ArrayHelper;
* @author Carsten Brandt
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ConstraintFinderTrait;
@@ -79,9 +80,14 @@ class Schema extends \yii\db\Schema
'Operation would have caused one or more unique constraint violations' => IntegrityException::class,
];
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = '"';
+
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -99,7 +105,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -148,7 +154,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -165,7 +171,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -195,7 +201,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -203,7 +209,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -211,7 +217,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableChecks($tableName)
@@ -220,7 +226,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
@@ -237,28 +243,6 @@ class Schema extends \yii\db\Schema
}
/**
- * Quotes a table name for use in a query.
- * A simple table name has no schema prefix.
- * @param string $name table name
- * @return string the properly quoted table name
- */
- public function quoteSimpleTableName($name)
- {
- return strpos($name, '"') !== false ? $name : '"' . $name . '"';
- }
-
- /**
- * Quotes a column name for use in a query.
- * A simple column name has no prefix.
- * @param string $name column name
- * @return string the properly quoted column name
- */
- public function quoteSimpleColumnName($name)
- {
- return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"';
- }
-
- /**
* Creates a query builder for the CUBRID database.
* @return QueryBuilder query builder instance
*/
diff --git a/framework/db/cubrid/conditions/LikeConditionBuilder.php b/framework/db/cubrid/conditions/LikeConditionBuilder.php
new file mode 100644
index 0000000..c6ea75e
--- /dev/null
+++ b/framework/db/cubrid/conditions/LikeConditionBuilder.php
@@ -0,0 +1,29 @@
+ '!%',
+ '_' => '!_',
+ '!' => '!!',
+ ];
+}
diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php
index e66c7b8..b61bf75 100644
--- a/framework/db/mssql/QueryBuilder.php
+++ b/framework/db/mssql/QueryBuilder.php
@@ -8,7 +8,7 @@
namespace yii\db\mssql;
use yii\base\InvalidArgumentException;
-use yii\base\NotSupportedException;
+use yii\db\Constraint;
use yii\db\Expression;
/**
@@ -30,6 +30,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'nchar(1)',
Schema::TYPE_STRING => 'nvarchar(255)',
Schema::TYPE_TEXT => 'nvarchar(max)',
+ Schema::TYPE_TINYINT => 'tinyint',
Schema::TYPE_SMALLINT => 'smallint',
Schema::TYPE_INTEGER => 'int',
Schema::TYPE_BIGINT => 'bigint',
@@ -45,17 +46,17 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_MONEY => 'decimal(19,4)',
];
+
/**
* {@inheritdoc}
*/
- protected $likeEscapingReplacements = [
- '%' => '[%]',
- '_' => '[_]',
- '[' => '[[]',
- ']' => '[]]',
- '\\' => '[\\]',
- ];
-
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\conditions\InCondition' => 'yii\db\mssql\conditions\InConditionBuilder',
+ 'yii\db\conditions\LikeCondition' => 'yii\db\mssql\conditions\LikeConditionBuilder',
+ ]);
+ }
/**
* {@inheritdoc}
@@ -67,7 +68,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
return $orderBy === '' ? $sql : $sql . $this->separator . $orderBy;
}
- if ($this->isOldMssql()) {
+ if (version_compare($this->db->getSchema()->getServerVersion(), '11', '<')) {
return $this->oldBuildOrderByAndLimit($sql, $orderBy, $limit, $offset, $params);
}
@@ -179,7 +180,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public function addDefaultValue($name, $table, $column, $value)
{
@@ -189,7 +190,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public function dropDefaultValue($name, $table)
{
@@ -304,70 +305,14 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @var bool whether MSSQL used is old.
- */
- private $_oldMssql;
-
- /**
* @return bool whether the version of the MSSQL being used is older than 2012.
* @throws \yii\base\InvalidConfigException
* @throws \yii\db\Exception
+ * @deprecated 2.0.14 Use [[Schema::getServerVersion]] with [[\version_compare()]].
*/
protected function isOldMssql()
{
- if ($this->_oldMssql === null) {
- $pdo = $this->db->getSlavePdo();
- $version = explode('.', $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION));
- $this->_oldMssql = $version[0] < 11;
- }
-
- return $this->_oldMssql;
- }
-
- /**
- * {@inheritdoc}
- * @throws NotSupportedException if `$columns` is an array
- */
- protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
- {
- if (is_array($columns)) {
- throw new NotSupportedException(__METHOD__ . ' is not supported by MSSQL.');
- }
-
- return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
- }
-
- /**
- * Builds SQL for IN condition.
- *
- * @param string $operator
- * @param array $columns
- * @param array $values
- * @param array $params
- * @return string SQL
- */
- protected function buildCompositeInCondition($operator, $columns, $values, &$params)
- {
- $quotedColumns = [];
- foreach ($columns as $i => $column) {
- $quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
- }
- $vss = [];
- foreach ($values as $value) {
- $vs = [];
- foreach ($columns as $i => $column) {
- if (isset($value[$column])) {
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = $value[$column];
- $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
- } else {
- $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
- }
- }
- $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
- }
-
- return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
+ return version_compare($this->db->getSchema()->getServerVersion(), '11', '<');
}
/**
@@ -392,8 +337,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($columns as $name => $value) {
// @see https://github.com/yiisoft/yii2/issues/12599
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && is_string($value)) {
- $phName = self::PARAM_PREFIX . count($params);
- $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", [$phName => $value]);
+ $exParams = [];
+ $phName = $this->bindParam($value, $exParams);
+ $columns[$name] = new Expression("CONVERT(VARBINARY, $phName)", $exParams);
}
}
}
@@ -411,6 +357,63 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
+ * @see https://docs.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql
+ * @see http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ /** @var Constraint[] $constraints */
+ list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
+ if (empty($uniqueNames)) {
+ return $this->insert($table, $insertColumns, $params);
+ }
+
+ $onCondition = ['or'];
+ $quotedTableName = $this->db->quoteTableName($table);
+ foreach ($constraints as $constraint) {
+ $constraintCondition = ['and'];
+ foreach ($constraint->columnNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ $constraintCondition[] = "$quotedTableName.$quotedName=[EXCLUDED].$quotedName";
+ }
+ $onCondition[] = $constraintCondition;
+ }
+ $on = $this->buildCondition($onCondition, $params);
+ list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
+ $mergeSql = 'MERGE ' . $this->db->quoteTableName($table) . ' WITH (HOLDLOCK) '
+ . 'USING (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') AS [EXCLUDED] (' . implode(', ', $insertNames) . ') '
+ . 'ON ' . $on;
+ $insertValues = [];
+ foreach ($insertNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '[EXCLUDED].' . $quotedName;
+ }
+ $insertValues[] = $quotedName;
+ }
+ $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
+ . ' VALUES (' . implode(', ', $insertValues) . ')';
+ if ($updateColumns === false) {
+ return "$mergeSql WHEN NOT MATCHED THEN $insertSql;";
+ }
+
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '[EXCLUDED].' . $quotedName;
+ }
+ $updateColumns[$name] = new Expression($quotedName);
+ }
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ $updateSql = 'UPDATE SET ' . implode(', ', $updates);
+ return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql;";
+ }
+
+ /**
+ * {@inheritdoc}
*/
public function update($table, $columns, $condition, &$params)
{
diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php
index ade63ac..3d673ae 100644
--- a/framework/db/mssql/Schema.php
+++ b/framework/db/mssql/Schema.php
@@ -10,6 +10,7 @@ namespace yii\db\mssql;
use yii\db\CheckConstraint;
use yii\db\ColumnSchema;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\DefaultValueConstraint;
use yii\db\ForeignKeyConstraint;
@@ -23,7 +24,7 @@ use yii\helpers\ArrayHelper;
* @author Timur Ruziev
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ViewFinderTrait;
use ConstraintFinderTrait;
@@ -44,7 +45,7 @@ class Schema extends \yii\db\Schema
'decimal' => self::TYPE_DECIMAL,
'smallmoney' => self::TYPE_MONEY,
'int' => self::TYPE_INTEGER,
- 'tinyint' => self::TYPE_SMALLINT,
+ 'tinyint' => self::TYPE_TINYINT,
'money' => self::TYPE_MONEY,
// approximate numbers
'float' => self::TYPE_FLOAT,
@@ -79,6 +80,15 @@ class Schema extends \yii\db\Schema
'table' => self::TYPE_STRING,
];
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = ['[', ']'];
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnQuoteCharacter = ['[', ']'];
+
/**
* Resolves the table name and schema name (if any).
@@ -117,7 +127,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
*/
protected function findSchemaNames()
@@ -134,7 +144,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -153,7 +163,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -169,7 +179,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -177,7 +187,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -185,7 +195,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -224,7 +234,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -232,7 +242,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableChecks($tableName)
{
@@ -240,7 +250,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableDefaultValues($tableName)
{
@@ -272,28 +282,6 @@ SQL;
}
/**
- * Quotes a table name for use in a query.
- * A simple table name has no schema prefix.
- * @param string $name table name.
- * @return string the properly quoted table name.
- */
- public function quoteSimpleTableName($name)
- {
- return strpos($name, '[') === false ? "[{$name}]" : $name;
- }
-
- /**
- * Quotes a column name for use in a query.
- * A simple column name has no prefix.
- * @param string $name column name.
- * @return string the properly quoted column name.
- */
- public function quoteSimpleColumnName($name)
- {
- return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name;
- }
-
- /**
* Creates a query builder for the MSSQL database.
* @return QueryBuilder query builder interface.
*/
diff --git a/framework/db/mssql/conditions/InConditionBuilder.php b/framework/db/mssql/conditions/InConditionBuilder.php
new file mode 100644
index 0000000..7892b54
--- /dev/null
+++ b/framework/db/mssql/conditions/InConditionBuilder.php
@@ -0,0 +1,58 @@
+
+ * @since 2.0.14
+ */
+class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
+{
+ /**
+ * {@inheritdoc}
+ * @throws NotSupportedException if `$columns` is an array
+ */
+ protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
+ {
+ if (is_array($columns)) {
+ throw new NotSupportedException(__METHOD__ . ' is not supported by MSSQL.');
+ }
+
+ return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildCompositeInCondition($operator, $columns, $values, &$params)
+ {
+ $quotedColumns = [];
+ foreach ($columns as $i => $column) {
+ $quotedColumns[$i] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
+ }
+ $vss = [];
+ foreach ($values as $value) {
+ $vs = [];
+ foreach ($columns as $i => $column) {
+ if (isset($value[$column])) {
+ $phName = $this->queryBuilder->bindParam($value[$column], $params);
+ $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
+ } else {
+ $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
+ }
+ }
+ $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
+ }
+
+ return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
+ }
+}
diff --git a/framework/db/mssql/conditions/LikeConditionBuilder.php b/framework/db/mssql/conditions/LikeConditionBuilder.php
new file mode 100644
index 0000000..f96cd2a
--- /dev/null
+++ b/framework/db/mssql/conditions/LikeConditionBuilder.php
@@ -0,0 +1,25 @@
+ '[%]',
+ '_' => '[_]',
+ '[' => '[[]',
+ ']' => '[]]',
+ '\\' => '[\\]',
+ ];
+}
diff --git a/framework/db/mysql/JsonExpressionBuilder.php b/framework/db/mysql/JsonExpressionBuilder.php
new file mode 100644
index 0000000..2f3d11e
--- /dev/null
+++ b/framework/db/mysql/JsonExpressionBuilder.php
@@ -0,0 +1,48 @@
+
+ * @since 2.0.14
+ */
+class JsonExpressionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+ const PARAM_PREFIX = ':qp';
+
+
+ /**
+ * {@inheritdoc}
+ * @param JsonExpression|ExpressionInterface $expression the expression to be built
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $value = $expression->getValue();
+
+ if ($value instanceof Query) {
+ list ($sql, $params) = $this->queryBuilder->build($value, $params);
+ return "($sql)";
+ }
+
+ $placeholder = static::PARAM_PREFIX . count($params);
+ $params[$placeholder] = Json::encode($value);
+
+ return $placeholder;
+ }
+}
diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php
index 5e40407..fca872c 100644
--- a/framework/db/mysql/QueryBuilder.php
+++ b/framework/db/mysql/QueryBuilder.php
@@ -11,6 +11,7 @@ use yii\base\InvalidArgumentException;
use yii\base\NotSupportedException;
use yii\db\Exception;
use yii\db\Expression;
+use yii\db\Query;
/**
* QueryBuilder is the query builder for MySQL databases.
@@ -31,6 +32,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'char(1)',
Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'text',
+ Schema::TYPE_TINYINT => 'tinyint(3)',
Schema::TYPE_SMALLINT => 'smallint(6)',
Schema::TYPE_INTEGER => 'int(11)',
Schema::TYPE_BIGINT => 'bigint(20)',
@@ -44,10 +46,21 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_BINARY => 'blob',
Schema::TYPE_BOOLEAN => 'tinyint(1)',
Schema::TYPE_MONEY => 'decimal(19,4)',
+ Schema::TYPE_JSON => 'json'
];
/**
+ * {@inheritdoc}
+ */
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\JsonExpression' => 'yii\db\mysql\JsonExpressionBuilder',
+ ]);
+ }
+
+ /**
* Builds a SQL statement for renaming a column.
* @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
* @param string $oldName the old name of the column. The name will be properly quoted by the method.
@@ -121,7 +134,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public function dropUnique($name, $table)
{
@@ -129,7 +142,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by MySQL.
*/
public function addCheck($name, $table, $expression)
@@ -138,7 +151,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by MySQL.
*/
public function dropCheck($name, $table)
@@ -231,48 +244,45 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
- public function insert($table, $columns, &$params)
+ protected function prepareInsertValues($table, $columns, $params = [])
{
- $schema = $this->db->getSchema();
- if (($tableSchema = $schema->getTableSchema($table)) !== null) {
- $columnSchemas = $tableSchema->columns;
- } else {
- $columnSchemas = [];
- }
- $names = [];
- $placeholders = [];
- $values = ' DEFAULT VALUES';
- if ($columns instanceof \yii\db\Query) {
- [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
- } else {
- foreach ($columns as $name => $value) {
- $names[] = $schema->quoteColumnName($name);
- if ($value instanceof Expression) {
- $placeholders[] = $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
- } elseif ($value instanceof \yii\db\Query) {
- [$sql, $params] = $this->build($value, $params);
- $placeholders[] = "($sql)";
- } else {
- $phName = self::PARAM_PREFIX . count($params);
- $placeholders[] = $phName;
- $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
- }
- }
- if (empty($names) && $tableSchema !== null) {
+ list($names, $placeholders, $values, $params) = parent::prepareInsertValues($table, $columns, $params);
+ if (!$columns instanceof Query && empty($names)) {
+ $tableSchema = $this->db->getSchema()->getTableSchema($table);
+ if ($tableSchema !== null) {
$columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name];
foreach ($columns as $name) {
- $names[] = $schema->quoteColumnName($name);
+ $names[] = $this->db->quoteColumnName($name);
$placeholders[] = 'DEFAULT';
}
}
}
+ return [$names, $placeholders, $values, $params];
+ }
- return 'INSERT INTO ' . $schema->quoteTableName($table)
- . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
- . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
+ /**
+ * {@inheritdoc}
+ * @see https://downloads.mysql.com/docs/refman-5.1-en.pdf
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ $insertSql = $this->insert($table, $insertColumns, $params);
+ list($uniqueNames, , $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
+ if (empty($uniqueNames)) {
+ return $insertSql;
+ }
+
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $updateColumns[$name] = new Expression('VALUES(' . $this->db->quoteColumnName($name) . ')');
+ }
+ } elseif ($updateColumns === false) {
+ $name = $this->db->quoteColumnName(reset($uniqueNames));
+ $updateColumns = [$name => new Expression($this->db->quoteTableName($table) . '.' . $name)];
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ return $insertSql . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
}
/**
diff --git a/framework/db/mysql/Schema.php b/framework/db/mysql/Schema.php
index e7ba028..f4f6c64 100644
--- a/framework/db/mysql/Schema.php
+++ b/framework/db/mysql/Schema.php
@@ -11,6 +11,7 @@ use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\db\ColumnSchema;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\Exception;
use yii\db\Expression;
@@ -25,7 +26,7 @@ use yii\helpers\ArrayHelper;
* @author Qiang Xue
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ConstraintFinderTrait;
@@ -39,7 +40,7 @@ class Schema extends \yii\db\Schema
* @var array mapping from physical column types (keys) to abstract column types (values)
*/
public $typeMap = [
- 'tinyint' => self::TYPE_SMALLINT,
+ 'tinyint' => self::TYPE_TINYINT,
'bit' => self::TYPE_INTEGER,
'smallint' => self::TYPE_SMALLINT,
'mediumint' => self::TYPE_INTEGER,
@@ -67,10 +68,20 @@ class Schema extends \yii\db\Schema
'timestamp' => self::TYPE_TIMESTAMP,
'enum' => self::TYPE_STRING,
'varbinary' => self::TYPE_BINARY,
+ 'json' => self::TYPE_JSON,
];
/**
- * @inheritDoc
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = '`';
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnQuoteCharacter = '`';
+
+ /**
+ * {@inheritdoc}
*/
protected function resolveTableName($name)
{
@@ -88,7 +99,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -101,7 +112,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -117,7 +128,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -125,7 +136,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -133,7 +144,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -169,7 +180,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -177,7 +188,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableChecks($tableName)
@@ -186,7 +197,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
@@ -195,28 +206,6 @@ SQL;
}
/**
- * Quotes a table name for use in a query.
- * A simple table name has no schema prefix.
- * @param string $name table name
- * @return string the properly quoted table name
- */
- public function quoteSimpleTableName($name)
- {
- return strpos($name, '`') !== false ? $name : "`$name`";
- }
-
- /**
- * Quotes a column name for use in a query.
- * A simple column name has no prefix.
- * @param string $name column name
- * @return string the properly quoted column name
- */
- public function quoteSimpleColumnName($name)
- {
- return strpos($name, '`') !== false || $name === '*' ? $name : "`$name`";
- }
-
- /**
* Creates a query builder for the MySQL database.
* @return QueryBuilder query builder instance
*/
diff --git a/framework/db/oci/QueryBuilder.php b/framework/db/oci/QueryBuilder.php
index a2e6c95..829b1f1 100644
--- a/framework/db/oci/QueryBuilder.php
+++ b/framework/db/oci/QueryBuilder.php
@@ -9,9 +9,12 @@ namespace yii\db\oci;
use yii\base\InvalidArgumentException;
use yii\db\Connection;
+use yii\db\Constraint;
use yii\db\Exception;
use yii\db\Expression;
+use yii\db\Query;
use yii\helpers\StringHelper;
+use yii\db\ExpressionInterface;
/**
* QueryBuilder is the query builder for Oracle databases.
@@ -32,6 +35,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'CHAR(1)',
Schema::TYPE_STRING => 'VARCHAR2(255)',
Schema::TYPE_TEXT => 'CLOB',
+ Schema::TYPE_TINYINT => 'NUMBER(3)',
Schema::TYPE_SMALLINT => 'NUMBER(5)',
Schema::TYPE_INTEGER => 'NUMBER(10)',
Schema::TYPE_BIGINT => 'NUMBER(20)',
@@ -47,21 +51,17 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_MONEY => 'NUMBER(19,4)',
];
+
/**
* {@inheritdoc}
*/
- protected $likeEscapeCharacter = '!';
- /**
- * `\` is initialized in [[buildLikeCondition()]] method since
- * we need to choose replacement value based on [[\yii\db\Schema::quoteValue()]].
- * {@inheritdoc}
- */
- protected $likeEscapingReplacements = [
- '%' => '!%',
- '_' => '!_',
- '!' => '!!',
- ];
-
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\conditions\InCondition' => 'yii\db\oci\conditions\InConditionBuilder',
+ 'yii\db\conditions\LikeCondition' => 'yii\db\oci\conditions\LikeConditionBuilder',
+ ]);
+ }
/**
* {@inheritdoc}
@@ -184,48 +184,86 @@ EOD;
/**
* {@inheritdoc}
*/
- public function insert($table, $columns, &$params)
+ protected function prepareInsertValues($table, $columns, $params = [])
{
- $schema = $this->db->getSchema();
- if (($tableSchema = $schema->getTableSchema($table)) !== null) {
- $columnSchemas = $tableSchema->columns;
- } else {
- $columnSchemas = [];
- }
- $names = [];
- $placeholders = [];
- $values = ' DEFAULT VALUES';
- if ($columns instanceof \yii\db\Query) {
- [$names, $values, $params] = $this->prepareInsertSelectSubQuery($columns, $schema, $params);
- } else {
- foreach ($columns as $name => $value) {
- $names[] = $schema->quoteColumnName($name);
- if ($value instanceof Expression) {
- $placeholders[] = $value->expression;
- foreach ($value->params as $n => $v) {
- $params[$n] = $v;
- }
- } elseif ($value instanceof \yii\db\Query) {
- [$sql, $params] = $this->build($value, $params);
- $placeholders[] = "($sql)";
- } else {
- $phName = self::PARAM_PREFIX . count($params);
- $placeholders[] = $phName;
- $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value;
- }
- }
- if (empty($names) && $tableSchema !== null) {
+ list($names, $placeholders, $values, $params) = parent::prepareInsertValues($table, $columns, $params);
+ if (!$columns instanceof Query && empty($names)) {
+ $tableSchema = $this->db->getSchema()->getTableSchema($table);
+ if ($tableSchema !== null) {
$columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name];
foreach ($columns as $name) {
- $names[] = $schema->quoteColumnName($name);
+ $names[] = $this->db->quoteColumnName($name);
$placeholders[] = 'DEFAULT';
}
}
}
+ return [$names, $placeholders, $values, $params];
+ }
- return 'INSERT INTO ' . $schema->quoteTableName($table)
- . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '')
- . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
+ /**
+ * {@inheritdoc}
+ * @see https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ /** @var Constraint[] $constraints */
+ list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
+ if (empty($uniqueNames)) {
+ return $this->insert($table, $insertColumns, $params);
+ }
+
+ $onCondition = ['or'];
+ $quotedTableName = $this->db->quoteTableName($table);
+ foreach ($constraints as $constraint) {
+ $constraintCondition = ['and'];
+ foreach ($constraint->columnNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ $constraintCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
+ }
+ $onCondition[] = $constraintCondition;
+ }
+ $on = $this->buildCondition($onCondition, $params);
+ list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
+ if (!empty($placeholders)) {
+ $usingSelectValues = [];
+ foreach ($insertNames as $index => $name) {
+ $usingSelectValues[$name] = new Expression($placeholders[$index]);
+ }
+ $usingSubQuery = (new Query())
+ ->select($usingSelectValues)
+ ->from('DUAL');
+ list($usingValues, $params) = $this->build($usingSubQuery, $params);
+ }
+ $mergeSql = 'MERGE INTO ' . $this->db->quoteTableName($table) . ' '
+ . 'USING (' . (isset($usingValues) ? $usingValues : ltrim($values, ' ')) . ') "EXCLUDED" '
+ . 'ON ' . $on;
+ $insertValues = [];
+ foreach ($insertNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '"EXCLUDED".' . $quotedName;
+ }
+ $insertValues[] = $quotedName;
+ }
+ $insertSql = 'INSERT (' . implode(', ', $insertNames) . ')'
+ . ' VALUES (' . implode(', ', $insertValues) . ')';
+ if ($updateColumns === false) {
+ return "$mergeSql WHEN NOT MATCHED THEN $insertSql";
+ }
+
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '"EXCLUDED".' . $quotedName;
+ }
+ $updateColumns[$name] = new Expression($quotedName);
+ }
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ $updateSql = 'UPDATE SET ' . implode(', ', $updates);
+ return "$mergeSql WHEN MATCHED THEN $updateSql WHEN NOT MATCHED THEN $insertSql";
}
/**
@@ -248,7 +286,7 @@ EOD;
* @param array|\Generator $rows the rows to be batch inserted into the table
* @return string the batch INSERT SQL statement
*/
- public function batchInsert($table, $columns, $rows)
+ public function batchInsert($table, $columns, $rows, &$params = [])
{
if (empty($rows)) {
return '';
@@ -265,7 +303,7 @@ EOD;
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
- if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
+ if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -277,6 +315,8 @@ EOD;
$value = 0;
} elseif ($value === null) {
$value = 'NULL';
+ } elseif ($value instanceof ExpressionInterface) {
+ $value = $this->buildExpression($value, $params);
}
$vs[] = $value;
}
@@ -322,74 +362,4 @@ EOD;
{
return 'COMMENT ON TABLE ' . $this->db->quoteTableName($table) . " IS ''";
}
-
- /**
- * @inheritDoc
- */
- public function buildLikeCondition($operator, $operands, &$params)
- {
- if (!isset($this->likeEscapingReplacements['\\'])) {
- /*
- * Different pdo_oci8 versions may or may not implement PDO::quote(), so
- * yii\db\Schema::quoteValue() may or may not quote \.
- */
- $this->likeEscapingReplacements['\\'] = substr($this->db->quoteValue('\\'), 1, -1);
- }
-
- return parent::buildLikeCondition($operator, $operands, $params);
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildInCondition($operator, $operands, &$params)
- {
- $splitCondition = $this->splitInCondition($operator, $operands, $params);
- if ($splitCondition !== null) {
- return $splitCondition;
- }
-
- return parent::buildInCondition($operator, $operands, $params);
- }
-
- /**
- * Oracle DBMS does not support more than 1000 parameters in `IN` condition.
- * This method splits long `IN` condition into series of smaller ones.
- *
- * @param string $operator
- * @param array $operands
- * @param array $params
- * @return null|string null when split is not required. Otherwise - built SQL condition.
- * @throws Exception
- * @since 2.0.12
- */
- protected function splitInCondition($operator, $operands, &$params)
- {
- if (!isset($operands[0], $operands[1])) {
- throw new Exception("Operator '$operator' requires two operands.");
- }
-
- [$column, $values] = $operands;
-
- if ($values instanceof \Traversable) {
- $values = iterator_to_array($values);
- }
-
- if (!is_array($values)) {
- return null;
- }
-
- $maxParameters = 1000;
- $count = count($values);
- if ($count <= $maxParameters) {
- return null;
- }
-
- $condition = [($operator === 'IN') ? 'OR' : 'AND'];
- for ($i = 0; $i < $count; $i += $maxParameters) {
- $condition[] = [$operator, $column, array_slice($values, $i, $maxParameters)];
- }
-
- return $this->buildCondition(['AND', $condition], $params);
- }
}
diff --git a/framework/db/oci/Schema.php b/framework/db/oci/Schema.php
index b18638b..334612d 100644
--- a/framework/db/oci/Schema.php
+++ b/framework/db/oci/Schema.php
@@ -13,6 +13,7 @@ use yii\db\CheckConstraint;
use yii\db\ColumnSchema;
use yii\db\Connection;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\Expression;
use yii\db\ForeignKeyConstraint;
@@ -30,7 +31,7 @@ use yii\db\IntegrityException;
* @author Qiang Xue
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ConstraintFinderTrait;
@@ -42,6 +43,11 @@ class Schema extends \yii\db\Schema
'ORA-00001: unique constraint' => IntegrityException::class,
];
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = '"';
+
/**
* {@inheritdoc}
@@ -50,12 +56,16 @@ class Schema extends \yii\db\Schema
{
parent::init();
if ($this->defaultSchema === null) {
- $this->defaultSchema = strtoupper($this->db->username);
+ $username = $this->db->username;
+ if (empty($username)) {
+ $username = isset($this->db->masters[0]['username']) ? $this->db->masters[0]['username'] : '';
+ }
+ $this->defaultSchema = strtoupper($username);
}
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function resolveTableName($name)
{
@@ -73,7 +83,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @see https://docs.oracle.com/cd/B28359_01/server.111/b28337/tdpsg_user_accounts.htm
*/
protected function findSchemaNames()
@@ -89,7 +99,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -135,7 +145,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -150,7 +160,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -158,7 +168,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -166,7 +176,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -207,7 +217,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -215,7 +225,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableChecks($tableName)
{
@@ -223,7 +233,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
@@ -296,7 +306,11 @@ SELECT
A.DATA_TYPE,
A.DATA_PRECISION,
A.DATA_SCALE,
- A.DATA_LENGTH,
+ (
+ CASE A.CHAR_USED WHEN 'C' THEN A.CHAR_LENGTH
+ ELSE A.DATA_LENGTH
+ END
+ ) AS DATA_LENGTH,
A.NULLABLE,
A.DATA_DEFAULT,
COM.COMMENTS AS COLUMN_COMMENT
diff --git a/framework/db/oci/conditions/InConditionBuilder.php b/framework/db/oci/conditions/InConditionBuilder.php
new file mode 100644
index 0000000..131bf5b
--- /dev/null
+++ b/framework/db/oci/conditions/InConditionBuilder.php
@@ -0,0 +1,71 @@
+splitCondition($expression, $params);
+ if ($splitCondition !== null) {
+ return $splitCondition;
+ }
+
+ return parent::build($expression, $params);
+ }
+
+ /**
+ * Oracle DBMS does not support more than 1000 parameters in `IN` condition.
+ * This method splits long `IN` condition into series of smaller ones.
+ *
+ * @param ExpressionInterface|InCondition $condition the expression to be built.
+ * @param array $params the binding parameters.
+ * @return null|string null when split is not required. Otherwise - built SQL condition.
+ */
+ protected function splitCondition(InCondition $condition, &$params)
+ {
+ $operator = $condition->getOperator();
+ $values = $condition->getValues();
+ $column = $condition->getColumn();
+
+ if ($values instanceof \Traversable) {
+ $values = iterator_to_array($values);
+ }
+
+ if (!is_array($values)) {
+ return null;
+ }
+
+ $maxParameters = 1000;
+ $count = count($values);
+ if ($count <= $maxParameters) {
+ return null;
+ }
+
+ $slices = [];
+ for ($i = 0; $i < $count; $i += $maxParameters) {
+ $slices[] = $this->queryBuilder->createConditionFromArray([$operator, $column, array_slice($values, $i, $maxParameters)]);
+ }
+
+ return $this->queryBuilder->buildCondition([($operator === 'IN') ? 'OR' : 'AND', $slices], $params);
+ }
+}
diff --git a/framework/db/oci/conditions/LikeConditionBuilder.php b/framework/db/oci/conditions/LikeConditionBuilder.php
new file mode 100644
index 0000000..a8c0c2a
--- /dev/null
+++ b/framework/db/oci/conditions/LikeConditionBuilder.php
@@ -0,0 +1,48 @@
+ '!%',
+ '_' => '!_',
+ '!' => '!!',
+ ];
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ if (!isset($this->escapingReplacements['\\'])) {
+ /*
+ * Different pdo_oci8 versions may or may not implement PDO::quote(), so
+ * yii\db\Schema::quoteValue() may or may not quote \.
+ */
+ $this->escapingReplacements['\\'] = substr($this->queryBuilder->db->quoteValue('\\'), 1, -1);
+ }
+
+ return parent::build($expression, $params);
+ }
+}
diff --git a/framework/db/pgsql/ArrayExpressionBuilder.php b/framework/db/pgsql/ArrayExpressionBuilder.php
new file mode 100644
index 0000000..855cfb0
--- /dev/null
+++ b/framework/db/pgsql/ArrayExpressionBuilder.php
@@ -0,0 +1,149 @@
+
+ * @since 2.0.14
+ */
+class ArrayExpressionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * {@inheritdoc}
+ * @param ArrayExpression|ExpressionInterface $expression the expression to be built
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $value = $expression->getValue();
+
+ if ($value instanceof Query) {
+ list ($sql, $params) = $this->queryBuilder->build($value, $params);
+ return $this->buildSubqueryArray($sql, $expression);
+ }
+
+ $placeholders = $this->buildPlaceholders($expression, $params);
+ if (empty($placeholders)) {
+ return "'{}'";
+ }
+
+ return 'ARRAY[' . implode(', ', $placeholders) . ']' . $this->getTypehint($expression);
+ }
+
+ /**
+ * Builds placeholders array out of $expression values
+ * @param ExpressionInterface|ArrayExpression $expression
+ * @param array $params the binding parameters.
+ * @return array
+ */
+ protected function buildPlaceholders(ExpressionInterface $expression, &$params)
+ {
+ $value = $expression->getValue();
+
+ $placeholders = [];
+ if ($value === null || !is_array($value) && !$value instanceof \Traversable) {
+ return $placeholders;
+ }
+
+ if ($expression->getDimension() > 1) {
+ foreach ($value as $item) {
+ $placeholders[] = $this->build($this->unnestArrayExpression($expression, $item), $params);
+ }
+ return $placeholders;
+ }
+
+ foreach ($value as $item) {
+ if ($item instanceof Query) {
+ list ($sql, $params) = $this->queryBuilder->build($item, $params);
+ $placeholders[] = $this->buildSubqueryArray($sql, $expression);
+ continue;
+ }
+
+ $item = $this->typecastValue($expression, $item);
+ if ($item instanceof ExpressionInterface) {
+ $placeholders[] = $this->queryBuilder->buildExpression($item, $params);
+ continue;
+ }
+
+ $placeholders[] = $this->queryBuilder->bindParam($item, $params);
+ }
+
+ return $placeholders;
+ }
+
+ /**
+ * @param ArrayExpression $expression
+ * @param mixed $value
+ * @return ArrayExpression
+ */
+ private function unnestArrayExpression(ArrayExpression $expression, $value)
+ {
+ $expressionClass = get_class($expression);
+
+ return new $expressionClass($value, $expression->getType(), $expression->getDimension()-1);
+ }
+
+ /**
+ * @param ArrayExpression $expression
+ * @return string the typecast expression based on [[type]].
+ */
+ protected function getTypehint(ArrayExpression $expression)
+ {
+ if ($expression->getType() === null) {
+ return '';
+ }
+
+ $result = '::' . $expression->getType();
+ $result .= str_repeat('[]', $expression->getDimension());
+
+ return $result;
+ }
+
+ /**
+ * Build an array expression from a subquery SQL.
+ *
+ * @param string $sql the subquery SQL.
+ * @param ArrayExpression $expression
+ * @return string the subquery array expression.
+ */
+ protected function buildSubqueryArray($sql, ArrayExpression $expression)
+ {
+ return 'ARRAY(' . $sql . ')' . $this->getTypehint($expression);
+ }
+
+ /**
+ * Casts $value to use in $expression
+ *
+ * @param ArrayExpression $expression
+ * @param mixed $value
+ * @return JsonExpression
+ */
+ protected function typecastValue(ArrayExpression $expression, $value)
+ {
+ if ($value instanceof ExpressionInterface) {
+ return $value;
+ }
+
+ if (in_array($expression->getType(), [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
+ return new JsonExpression($value);
+ }
+
+ return $value;
+ }
+}
diff --git a/framework/db/pgsql/ArrayParser.php b/framework/db/pgsql/ArrayParser.php
new file mode 100644
index 0000000..8ce08b2
--- /dev/null
+++ b/framework/db/pgsql/ArrayParser.php
@@ -0,0 +1,109 @@
+
+ * @author Dmytro Naumenko
+ * @since 2.0.14
+ */
+class ArrayParser
+{
+ /**
+ * @var string Character used in array
+ */
+ private $delimiter = ',';
+
+
+ /**
+ * Convert array from PostgreSQL to PHP
+ *
+ * @param string $value string to be converted
+ * @return array|null
+ */
+ public function parse($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ if ($value === '{}') {
+ return [];
+ }
+
+ return $this->parseArray($value);
+ }
+
+ /**
+ * Pares PgSQL array encoded in string
+ *
+ * @param string $value
+ * @param int $i parse starting position
+ * @return array
+ */
+ private function parseArray($value, &$i = 0)
+ {
+ $result = [];
+ $len = strlen($value);
+ for (++$i; $i < $len; ++$i) {
+ switch ($value[$i]) {
+ case '{':
+ $result[] = $this->parseArray($value, $i);
+ break;
+ case '}':
+ break 2;
+ case $this->delimiter:
+ if (empty($result)) { // `{}` case
+ $result[] = null;
+ }
+ if (in_array($value[$i + 1], [$this->delimiter, '}'], true)) { // `{,}` case
+ $result[] = null;
+ }
+ break;
+ default:
+ $result[] = $this->parseString($value, $i);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Parses PgSQL encoded string
+ *
+ * @param string $value
+ * @param int $i parse starting position
+ * @return null|string
+ */
+ private function parseString($value, &$i)
+ {
+ $isQuoted = $value[$i] === '"';
+ $stringEndChars = $isQuoted ? ['"'] : [$this->delimiter, '}'];
+ $result = '';
+ $len = strlen($value);
+ for ($i += $isQuoted ? 1 : 0; $i < $len; ++$i) {
+ if (in_array($value[$i], ['\\', '"'], true) && in_array($value[$i + 1], [$value[$i], '"'], true)) {
+ ++$i;
+ } elseif (in_array($value[$i], $stringEndChars, true)) {
+ break;
+ }
+
+ $result .= $value[$i];
+ }
+
+ $i -= $isQuoted ? 0 : 1;
+
+ if (!$isQuoted && $result === 'NULL') {
+ $result = null;
+ }
+
+ return $result;
+ }
+}
diff --git a/framework/db/pgsql/ColumnSchema.php b/framework/db/pgsql/ColumnSchema.php
new file mode 100644
index 0000000..1b16242
--- /dev/null
+++ b/framework/db/pgsql/ColumnSchema.php
@@ -0,0 +1,112 @@
+
+ */
+class ColumnSchema extends \yii\db\ColumnSchema
+{
+ /**
+ * @var int the dimension of array. Defaults to 0, means this column is not an array.
+ */
+ public $dimension = 0;
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function dbTypecast($value)
+ {
+ if ($value instanceof ExpressionInterface) {
+ return $value;
+ }
+
+ if ($this->dimension > 0) {
+ return new ArrayExpression($value, $this->dbType, $this->dimension);
+ }
+ if (in_array($this->dbType, [Schema::TYPE_JSON, Schema::TYPE_JSONB], true)) {
+ return new JsonExpression($value, $this->type);
+ }
+
+ return $this->typecast($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function phpTypecast($value)
+ {
+ if ($this->dimension > 0) {
+ if (!is_array($value)) {
+ $value = $this->getArrayParser()->parse($value);
+ }
+ if (is_array($value)) {
+ array_walk_recursive($value, function (&$val, $key) {
+ $val = $this->phpTypecastValue($val);
+ });
+ }
+
+ return new ArrayExpression($value, $this->dbType, $this->dimension);
+ }
+
+ return $this->phpTypecastValue($value);
+ }
+
+ /**
+ * Casts $value after retrieving from the DBMS to PHP representation.
+ *
+ * @param string|null $value
+ * @return bool|mixed|null
+ */
+ protected function phpTypecastValue($value)
+ {
+ if ($value === null) {
+ return null;
+ }
+
+ switch ($this->type) {
+ case Schema::TYPE_BOOLEAN:
+ switch (strtolower($value)) {
+ case 't':
+ case 'true':
+ return true;
+ case 'f':
+ case 'false':
+ return false;
+ }
+ return (bool) $value;
+ case Schema::TYPE_JSON:
+ return json_decode($value, true);
+ }
+
+ return parent::phpTypecast($value);
+ }
+
+ /**
+ * Creates instance of ArrayParser
+ *
+ * @return ArrayParser
+ */
+ protected function getArrayParser()
+ {
+ static $parser = null;
+
+ if ($parser === null) {
+ $parser = new ArrayParser();
+ }
+
+ return $parser;
+ }
+}
diff --git a/framework/db/pgsql/JsonExpressionBuilder.php b/framework/db/pgsql/JsonExpressionBuilder.php
new file mode 100644
index 0000000..1a5cad0
--- /dev/null
+++ b/framework/db/pgsql/JsonExpressionBuilder.php
@@ -0,0 +1,58 @@
+
+ * @since 2.0.14
+ */
+class JsonExpressionBuilder implements ExpressionBuilderInterface
+{
+ use ExpressionBuilderTrait;
+
+
+ /**
+ * {@inheritdoc}
+ * @param JsonExpression|ExpressionInterface $expression the expression to be built
+ */
+ public function build(ExpressionInterface $expression, array &$params = [])
+ {
+ $value = $expression->getValue();
+
+ if ($value instanceof Query) {
+ list ($sql, $params) = $this->queryBuilder->build($value, $params);
+ return "($sql)" . $this->getTypecast($expression);
+ }
+
+ $placeholder = $this->queryBuilder->bindParam(Json::encode($value), $params);
+
+ return $placeholder . $this->getTypecast($expression);
+ }
+
+ /**
+ * @param JsonExpression $expression
+ * @return string the typecast expression based on [[type]].
+ */
+ protected function getTypecast(JsonExpression $expression)
+ {
+ if ($expression->getType() === null) {
+ return '';
+ }
+
+ return '::' . $expression->getType();
+ }
+}
diff --git a/framework/db/pgsql/QueryBuilder.php b/framework/db/pgsql/QueryBuilder.php
index d8afe45..acf4031 100644
--- a/framework/db/pgsql/QueryBuilder.php
+++ b/framework/db/pgsql/QueryBuilder.php
@@ -8,6 +8,11 @@
namespace yii\db\pgsql;
use yii\base\InvalidArgumentException;
+use yii\db\Constraint;
+use yii\db\Expression;
+use yii\db\ExpressionInterface;
+use yii\db\Query;
+use yii\db\PdoValue;
use yii\helpers\StringHelper;
/**
@@ -55,6 +60,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'char(1)',
Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'text',
+ Schema::TYPE_TINYINT => 'smallint',
Schema::TYPE_SMALLINT => 'smallint',
Schema::TYPE_INTEGER => 'integer',
Schema::TYPE_BIGINT => 'bigint',
@@ -68,32 +74,33 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_BINARY => 'bytea',
Schema::TYPE_BOOLEAN => 'boolean',
Schema::TYPE_MONEY => 'numeric(19,4)',
+ Schema::TYPE_JSON => 'jsonb',
];
+
/**
- * @var array map of query condition to builder methods.
- * These methods are used by [[buildCondition]] to build SQL conditions from array syntax.
+ * {@inheritdoc}
*/
- protected $conditionBuilders = [
- 'NOT' => 'buildNotCondition',
- 'AND' => 'buildAndCondition',
- 'OR' => 'buildAndCondition',
- 'BETWEEN' => 'buildBetweenCondition',
- 'NOT BETWEEN' => 'buildBetweenCondition',
- 'IN' => 'buildInCondition',
- 'NOT IN' => 'buildInCondition',
- 'LIKE' => 'buildLikeCondition',
- 'ILIKE' => 'buildLikeCondition',
- 'NOT LIKE' => 'buildLikeCondition',
- 'NOT ILIKE' => 'buildLikeCondition',
- 'OR LIKE' => 'buildLikeCondition',
- 'OR ILIKE' => 'buildLikeCondition',
- 'OR NOT LIKE' => 'buildLikeCondition',
- 'OR NOT ILIKE' => 'buildLikeCondition',
- 'EXISTS' => 'buildExistsCondition',
- 'NOT EXISTS' => 'buildExistsCondition',
- ];
+ protected function defaultConditionClasses()
+ {
+ return array_merge(parent::defaultConditionClasses(), [
+ 'ILIKE' => 'yii\db\conditions\LikeCondition',
+ 'NOT ILIKE' => 'yii\db\conditions\LikeCondition',
+ 'OR ILIKE' => 'yii\db\conditions\LikeCondition',
+ 'OR NOT ILIKE' => 'yii\db\conditions\LikeCondition',
+ ]);
+ }
+ /**
+ * {@inheritdoc}
+ */
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\ArrayExpression' => 'yii\db\pgsql\ArrayExpressionBuilder',
+ 'yii\db\JsonExpression' => 'yii\db\pgsql\JsonExpressionBuilder',
+ ]);
+ }
/**
* Builds a SQL statement for creating a new index.
@@ -249,6 +256,141 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
+ * @see https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
+ * @see https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
+ if (!is_bool($updateColumns)) {
+ $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
+ }
+ if (version_compare($this->db->getServerVersion(), '9.5', '<')) {
+ return $this->oldUpsert($table, $insertColumns, $updateColumns, $params);
+ }
+
+ return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
+ }
+
+ /**
+ * [[upsert()]] implementation for PostgreSQL 9.5 or higher.
+ * @param string $table
+ * @param array|Query $insertColumns
+ * @param array|bool $updateColumns
+ * @param array $params
+ * @return string
+ */
+ private function newUpsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ $insertSql = $this->insert($table, $insertColumns, $params);
+ list($uniqueNames, , $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
+ if (empty($uniqueNames)) {
+ return $insertSql;
+ }
+
+ if ($updateColumns === false) {
+ return "$insertSql ON CONFLICT DO NOTHING";
+ }
+
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $updateColumns[$name] = new Expression('EXCLUDED.' . $this->db->quoteColumnName($name));
+ }
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET ' . implode(', ', $updates);
+ }
+
+ /**
+ * [[upsert()]] implementation for PostgreSQL older than 9.5.
+ * @param string $table
+ * @param array|Query $insertColumns
+ * @param array|bool $updateColumns
+ * @param array $params
+ * @return string
+ */
+ private function oldUpsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ /** @var Constraint[] $constraints */
+ list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
+ if (empty($uniqueNames)) {
+ return $this->insert($table, $insertColumns, $params);
+ }
+
+ /** @var Schema $schema */
+ $schema = $this->db->getSchema();
+ if (!$insertColumns instanceof Query) {
+ $tableSchema = $schema->getTableSchema($table);
+ $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
+ foreach ($insertColumns as $name => $value) {
+ // NULLs and numeric values must be type hinted in order to be used in SET assigments
+ // NVM, let's cast them all
+ if (isset($columnSchemas[$name])) {
+ $phName = self::PARAM_PREFIX . count($params);
+ $params[$phName] = $value;
+ $insertColumns[$name] = new Expression("CAST($phName AS {$columnSchemas[$name]->dbType})");
+ }
+ }
+ }
+ list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
+ $updateCondition = ['or'];
+ $insertCondition = ['or'];
+ $quotedTableName = $schema->quoteTableName($table);
+ foreach ($constraints as $constraint) {
+ $constraintUpdateCondition = ['and'];
+ $constraintInsertCondition = ['and'];
+ foreach ($constraint->columnNames as $name) {
+ $quotedName = $schema->quoteColumnName($name);
+ $constraintUpdateCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
+ $constraintInsertCondition[] = "\"upsert\".$quotedName=\"EXCLUDED\".$quotedName";
+ }
+ $updateCondition[] = $constraintUpdateCondition;
+ $insertCondition[] = $constraintInsertCondition;
+ }
+ $withSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
+ . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ')';
+ if ($updateColumns === false) {
+ $selectSubQuery = (new Query())
+ ->select(new Expression('1'))
+ ->from($table)
+ ->where($updateCondition);
+ $insertSelectSubQuery = (new Query())
+ ->select($insertNames)
+ ->from('EXCLUDED')
+ ->where(['not exists', $selectSubQuery]);
+ $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
+ return "$withSql $insertSql";
+ }
+
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = '"EXCLUDED".' . $quotedName;
+ }
+ $updateColumns[$name] = new Expression($quotedName);
+ }
+ }
+ list($updates, $params) = $this->prepareUpdateSets($table, $updateColumns, $params);
+ $updateSql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $updates)
+ . ' FROM "EXCLUDED" ' . $this->buildWhere($updateCondition, $params)
+ . ' RETURNING ' . $this->db->quoteTableName($table) .'.*';
+ $selectUpsertSubQuery = (new Query())
+ ->select(new Expression('1'))
+ ->from('upsert')
+ ->where($insertCondition);
+ $insertSelectSubQuery = (new Query())
+ ->select($insertNames)
+ ->from('EXCLUDED')
+ ->where(['not exists', $selectUpsertSubQuery]);
+ $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
+ return "$withSql, \"upsert\" AS ($updateSql) $insertSql";
+ }
+
+ /**
+ * {@inheritdoc}
*/
public function update($table, $columns, $condition, &$params)
{
@@ -257,8 +399,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
+ *
* @param string $table the table that data will be saved into.
- * @param array|\yii\db\Query $columns the column data (name => value) to be saved into the table or instance
+ * @param array|Query $columns the column data (name => value) to be saved into the table or instance
* of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement.
* Passing of [[yii\db\Query|Query]] is available since version 2.0.11.
* @return array normalized columns
@@ -266,7 +409,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/
private function normalizeTableRowData($table, $columns)
{
- if ($columns instanceof \yii\db\Query) {
+ if ($columns instanceof Query) {
return $columns;
}
@@ -274,7 +417,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
$columnSchemas = $tableSchema->columns;
foreach ($columns as $name => $value) {
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && is_string($value)) {
- $columns[$name] = [$value, \PDO::PARAM_LOB]; // explicitly setup PDO param type for binary column
+ $columns[$name] = new PdoValue($value, \PDO::PARAM_LOB); // explicitly setup PDO param type for binary column
}
}
}
@@ -285,7 +428,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
*/
- public function batchInsert($table, $columns, $rows)
+ public function batchInsert($table, $columns, $rows, &$params = [])
{
if (empty($rows)) {
return '';
@@ -302,7 +445,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
- if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) {
+ if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -316,6 +459,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
$value = 'FALSE';
} elseif ($value === null) {
$value = 'NULL';
+ } elseif ($value instanceof ExpressionInterface) {
+ $value = $this->buildExpression($value, $params);
}
$vs[] = $value;
}
diff --git a/framework/db/pgsql/Schema.php b/framework/db/pgsql/Schema.php
index f2da250..fc26484 100644
--- a/framework/db/pgsql/Schema.php
+++ b/framework/db/pgsql/Schema.php
@@ -10,6 +10,7 @@ namespace yii\db\pgsql;
use yii\base\NotSupportedException;
use yii\db\CheckConstraint;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\Expression;
use yii\db\ForeignKeyConstraint;
@@ -25,16 +26,22 @@ use yii\helpers\ArrayHelper;
* @author Gevik Babakhani
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ViewFinderTrait;
use ConstraintFinderTrait;
+ const TYPE_JSONB = 'jsonb';
+
/**
* @var string the default schema used for the current session.
*/
public $defaultSchema = 'public';
/**
+ * {@inheritdoc}
+ */
+ public $columnSchemaClass = 'yii\db\pgsql\ColumnSchema';
+ /**
* @var array mapping from physical column types (keys) to abstract
* column types (values)
* @see http://www.postgresql.org/docs/current/static/datatype.html#DATATYPE-TABLE
@@ -113,14 +120,19 @@ class Schema extends \yii\db\Schema
'unknown' => self::TYPE_STRING,
'uuid' => self::TYPE_STRING,
- 'json' => self::TYPE_STRING,
- 'jsonb' => self::TYPE_STRING,
+ 'json' => self::TYPE_JSON,
+ 'jsonb' => self::TYPE_JSON,
'xml' => self::TYPE_STRING,
];
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = '"';
+
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function resolveTableName($name)
{
@@ -138,7 +150,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findSchemaNames()
{
@@ -153,7 +165,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -171,7 +183,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -186,7 +198,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -194,7 +206,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -202,7 +214,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -246,7 +258,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -254,7 +266,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableChecks($tableName)
{
@@ -262,7 +274,7 @@ SQL;
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
@@ -300,17 +312,6 @@ SQL;
}
/**
- * Quotes a table name for use in a query.
- * A simple table name has no schema prefix.
- * @param string $name table name
- * @return string the properly quoted table name
- */
- public function quoteSimpleTableName($name)
- {
- return strpos($name, '"') !== false ? $name : '"' . $name . '"';
- }
-
- /**
* {@inheritdoc]
*/
protected function findViewNames($schema = '')
@@ -468,14 +469,18 @@ SELECT
d.nspname AS table_schema,
c.relname AS table_name,
a.attname AS column_name,
- t.typname AS data_type,
+ COALESCE(td.typname, tb.typname, t.typname) AS data_type,
+ COALESCE(td.typtype, tb.typtype, t.typtype) AS type_type,
a.attlen AS character_maximum_length,
pg_catalog.col_description(c.oid, a.attnum) AS column_comment,
a.atttypmod AS modifier,
a.attnotnull = false AS is_nullable,
CAST(pg_get_expr(ad.adbin, ad.adrelid) AS varchar) AS column_default,
coalesce(pg_get_expr(ad.adbin, ad.adrelid) ~ 'nextval',false) AS is_autoinc,
- array_to_string((select array_agg(enumlabel) from pg_enum where enumtypid=a.atttypid)::varchar[],',') as enum_values,
+ CASE WHEN COALESCE(td.typtype, tb.typtype, t.typtype) = 'e'::char
+ THEN array_to_string((SELECT array_agg(enumlabel) FROM pg_enum WHERE enumtypid = COALESCE(td.oid, tb.oid, a.atttypid))::varchar[], ',')
+ ELSE NULL
+ END AS enum_values,
CASE atttypid
WHEN 21 /*int2*/ THEN 16
WHEN 23 /*int4*/ THEN 32
@@ -502,22 +507,24 @@ SELECT
information_schema._pg_char_max_length(information_schema._pg_truetypid(a, t), information_schema._pg_truetypmod(a, t))
AS numeric
) AS size,
- a.attnum = any (ct.conkey) as is_pkey
+ a.attnum = any (ct.conkey) as is_pkey,
+ COALESCE(NULLIF(a.attndims, 0), NULLIF(t.typndims, 0), (t.typcategory='A')::int) AS dimension
FROM
pg_class c
LEFT JOIN pg_attribute a ON a.attrelid = c.oid
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
LEFT JOIN pg_type t ON a.atttypid = t.oid
+ LEFT JOIN pg_type tb ON (a.attndims > 0 OR t.typcategory='A') AND t.typelem > 0 AND t.typelem = tb.oid OR t.typbasetype > 0 AND t.typbasetype = tb.oid
+ LEFT JOIN pg_type td ON t.typndims > 0 AND t.typbasetype > 0 AND tb.typelem = td.oid
LEFT JOIN pg_namespace d ON d.oid = c.relnamespace
- LEFT join pg_constraint ct on ct.conrelid=c.oid and ct.contype='p'
+ LEFT JOIN pg_constraint ct ON ct.conrelid = c.oid AND ct.contype = 'p'
WHERE
- a.attnum > 0 and t.typname != ''
- and c.relname = {$tableName}
- and d.nspname = {$schemaName}
+ a.attnum > 0 AND t.typname != ''
+ AND c.relname = {$tableName}
+ AND d.nspname = {$schemaName}
ORDER BY
a.attnum;
SQL;
-
$columns = $this->db->createCommand($sql)->queryAll();
if (empty($columns)) {
return false;
@@ -542,7 +549,7 @@ SQL;
} elseif (stripos($column->dbType, 'bit') === 0 || stripos($column->dbType, 'varbit') === 0) {
$column->defaultValue = bindec(trim($column->defaultValue, 'B\''));
} elseif (preg_match("/^'(.*?)'::/", $column->defaultValue, $matches)) {
- $column->defaultValue = $matches[1];
+ $column->defaultValue = $column->phpTypecast($matches[1]);
} elseif (preg_match('/^(\()?(.*?)(?(1)\))(?:::.+)?$/', $column->defaultValue, $matches)) {
if ($matches[2] === 'NULL') {
$column->defaultValue = null;
@@ -565,6 +572,7 @@ SQL;
*/
protected function loadColumnSchema($info)
{
+ /** @var ColumnSchema $column */
$column = $this->createColumnSchema();
$column->allowNull = $info['is_nullable'];
$column->autoIncrement = $info['is_autoinc'];
@@ -578,6 +586,7 @@ SQL;
$column->precision = $info['numeric_precision'];
$column->scale = $info['numeric_scale'];
$column->size = $info['size'] === null ? null : (int) $info['size'];
+ $column->dimension = (int)$info['dimension'];
if (isset($this->typeMap[$column->dbType])) {
$column->type = $this->typeMap[$column->dbType];
} else {
diff --git a/framework/db/sqlite/QueryBuilder.php b/framework/db/sqlite/QueryBuilder.php
index 9a47b6a..21440b7 100644
--- a/framework/db/sqlite/QueryBuilder.php
+++ b/framework/db/sqlite/QueryBuilder.php
@@ -7,9 +7,12 @@
namespace yii\db\sqlite;
-use yii\base\InvalidArgumentException;
+use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\Connection;
+use yii\db\Constraint;
+use yii\db\Expression;
+use yii\db\ExpressionInterface;
use yii\db\Query;
use yii\helpers\StringHelper;
@@ -32,6 +35,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_CHAR => 'char(1)',
Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'text',
+ Schema::TYPE_TINYINT => 'tinyint',
Schema::TYPE_SMALLINT => 'smallint',
Schema::TYPE_INTEGER => 'integer',
Schema::TYPE_BIGINT => 'bigint',
@@ -47,11 +51,63 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_MONEY => 'decimal(19,4)',
];
+
/**
* {@inheritdoc}
*/
- protected $likeEscapeCharacter = '\\';
+ protected function defaultExpressionBuilders()
+ {
+ return array_merge(parent::defaultExpressionBuilders(), [
+ 'yii\db\conditions\LikeCondition' => 'yii\db\sqlite\conditions\LikeConditionBuilder',
+ 'yii\db\conditions\InCondition' => 'yii\db\sqlite\conditions\InConditionBuilder',
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374
+ */
+ public function upsert($table, $insertColumns, $updateColumns, &$params)
+ {
+ /** @var Constraint[] $constraints */
+ list($uniqueNames, $insertNames, $updateNames) = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
+ if (empty($uniqueNames)) {
+ return $this->insert($table, $insertColumns, $params);
+ }
+
+ list(, $placeholders, $values, $params) = $this->prepareInsertValues($table, $insertColumns, $params);
+ $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
+ . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
+ . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
+ if ($updateColumns === false) {
+ return $insertSql;
+ }
+ $updateCondition = ['or'];
+ $quotedTableName = $this->db->quoteTableName($table);
+ foreach ($constraints as $constraint) {
+ $constraintCondition = ['and'];
+ foreach ($constraint->columnNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
+ }
+ $updateCondition[] = $constraintCondition;
+ }
+ if ($updateColumns === true) {
+ $updateColumns = [];
+ foreach ($updateNames as $name) {
+ $quotedName = $this->db->quoteColumnName($name);
+ if (strrpos($quotedName, '.') === false) {
+ $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
+ }
+ $updateColumns[$name] = new Expression($quotedName);
+ }
+ }
+ $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
+ . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') '
+ . $this->update($table, $updateColumns, $updateCondition, $params);
+ return "$updateSql; $insertSql;";
+ }
/**
* Generates a batch INSERT SQL statement.
@@ -73,7 +129,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
* @param array|\Generator $rows the rows to be batch inserted into the table
* @return string the batch INSERT SQL statement
*/
- public function batchInsert($table, $columns, $rows)
+ public function batchInsert($table, $columns, $rows, &$params = [])
{
if (empty($rows)) {
return '';
@@ -82,8 +138,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
// SQLite supports batch insert natively since 3.7.11
// http://www.sqlite.org/releaselog/3_7_11.html
$this->db->open(); // ensure pdo is not null
- if (version_compare($this->db->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '3.7.11', '>=')) {
- return parent::batchInsert($table, $columns, $rows);
+ if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
+ return parent::batchInsert($table, $columns, $rows, $params);
}
$schema = $this->db->getSchema();
@@ -97,7 +153,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($rows as $row) {
$vs = [];
foreach ($row as $i => $value) {
- if (!is_array($value) && isset($columnSchemas[$columns[$i]])) {
+ if (isset($columnSchemas[$columns[$i]])) {
$value = $columnSchemas[$columns[$i]]->dbTypecast($value);
}
if (is_string($value)) {
@@ -109,6 +165,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
$value = 0;
} elseif ($value === null) {
$value = 'NULL';
+ } elseif ($value instanceof ExpressionInterface) {
+ $value = $this->buildExpression($value, $params);
}
$vs[] = $value;
}
@@ -304,7 +362,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function addUnique($name, $table, $columns)
@@ -313,7 +371,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function dropUnique($name, $table)
@@ -322,7 +380,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function addCheck($name, $table, $expression)
@@ -331,7 +389,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function dropCheck($name, $table)
@@ -340,7 +398,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function addDefaultValue($name, $table, $column, $value)
@@ -349,7 +407,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException this is not supported by SQLite.
*/
public function dropDefaultValue($name, $table)
@@ -419,52 +477,6 @@ class QueryBuilder extends \yii\db\QueryBuilder
/**
* {@inheritdoc}
- * @throws NotSupportedException if `$columns` is an array
- */
- protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
- {
- if (is_array($columns)) {
- throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
- }
-
- return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
- }
-
- /**
- * Builds SQL for IN condition.
- *
- * @param string $operator
- * @param array $columns
- * @param array $values
- * @param array $params
- * @return string SQL
- */
- protected function buildCompositeInCondition($operator, $columns, $values, &$params)
- {
- $quotedColumns = [];
- foreach ($columns as $i => $column) {
- $quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column;
- }
- $vss = [];
- foreach ($values as $value) {
- $vs = [];
- foreach ($columns as $i => $column) {
- if (isset($value[$column])) {
- $phName = self::PARAM_PREFIX . count($params);
- $params[$phName] = $value[$column];
- $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
- } else {
- $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
- }
- }
- $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
- }
-
- return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
- }
-
- /**
- * {@inheritdoc}
*/
public function build($query, $params = [])
{
diff --git a/framework/db/sqlite/Schema.php b/framework/db/sqlite/Schema.php
index 5a7392c..d6dd66b 100644
--- a/framework/db/sqlite/Schema.php
+++ b/framework/db/sqlite/Schema.php
@@ -11,6 +11,7 @@ use yii\base\NotSupportedException;
use yii\db\CheckConstraint;
use yii\db\ColumnSchema;
use yii\db\Constraint;
+use yii\db\ConstraintFinderInterface;
use yii\db\ConstraintFinderTrait;
use yii\db\Expression;
use yii\db\ForeignKeyConstraint;
@@ -29,7 +30,7 @@ use yii\helpers\ArrayHelper;
* @author Qiang Xue
* @since 2.0
*/
-class Schema extends \yii\db\Schema
+class Schema extends \yii\db\Schema implements ConstraintFinderInterface
{
use ConstraintFinderTrait;
@@ -37,7 +38,7 @@ class Schema extends \yii\db\Schema
* @var array mapping from physical column types (keys) to abstract column types (values)
*/
public $typeMap = [
- 'tinyint' => self::TYPE_SMALLINT,
+ 'tinyint' => self::TYPE_TINYINT,
'bit' => self::TYPE_SMALLINT,
'boolean' => self::TYPE_BOOLEAN,
'bool' => self::TYPE_BOOLEAN,
@@ -67,9 +68,18 @@ class Schema extends \yii\db\Schema
'enum' => self::TYPE_STRING,
];
+ /**
+ * {@inheritdoc}
+ */
+ protected $tableQuoteCharacter = '`';
+ /**
+ * {@inheritdoc}
+ */
+ protected $columnQuoteCharacter = '`';
+
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function findTableNames($schema = '')
{
@@ -78,7 +88,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableSchema($name)
{
@@ -95,7 +105,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTablePrimaryKey($tableName)
{
@@ -103,7 +113,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableForeignKeys($tableName)
{
@@ -126,7 +136,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableIndexes($tableName)
{
@@ -134,7 +144,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableUniques($tableName)
{
@@ -142,7 +152,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function loadTableChecks($tableName)
{
@@ -181,7 +191,7 @@ class Schema extends \yii\db\Schema
}
/**
- * @inheritDoc
+ * {@inheritdoc}
* @throws NotSupportedException if this method is called.
*/
protected function loadTableDefaultValues($tableName)
@@ -190,28 +200,6 @@ class Schema extends \yii\db\Schema
}
/**
- * Quotes a table name for use in a query.
- * A simple table name has no schema prefix.
- * @param string $name table name
- * @return string the properly quoted table name
- */
- public function quoteSimpleTableName($name)
- {
- return strpos($name, '`') !== false ? $name : "`$name`";
- }
-
- /**
- * Quotes a column name for use in a query.
- * A simple column name has no prefix.
- * @param string $name column name
- * @return string the properly quoted column name
- */
- public function quoteSimpleColumnName($name)
- {
- return strpos($name, '`') !== false || $name === '*' ? $name : "`$name`";
- }
-
- /**
* Creates a query builder for the MySQL database.
* This method may be overridden by child classes to create a DBMS-specific query builder.
* @return QueryBuilder query builder instance
diff --git a/framework/db/sqlite/SqlTokenizer.php b/framework/db/sqlite/SqlTokenizer.php
index e153820..7a8aecb 100644
--- a/framework/db/sqlite/SqlTokenizer.php
+++ b/framework/db/sqlite/SqlTokenizer.php
@@ -19,7 +19,7 @@ namespace yii\db\sqlite;
class SqlTokenizer extends \yii\db\SqlTokenizer
{
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isWhitespace(&$length)
{
@@ -36,7 +36,7 @@ class SqlTokenizer extends \yii\db\SqlTokenizer
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isComment(&$length)
{
@@ -60,7 +60,7 @@ class SqlTokenizer extends \yii\db\SqlTokenizer
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isOperator(&$length, &$content)
{
@@ -95,7 +95,7 @@ class SqlTokenizer extends \yii\db\SqlTokenizer
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isIdentifier(&$length, &$content)
{
@@ -127,7 +127,7 @@ class SqlTokenizer extends \yii\db\SqlTokenizer
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isStringLiteral(&$length, &$content)
{
@@ -148,7 +148,7 @@ class SqlTokenizer extends \yii\db\SqlTokenizer
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
protected function isKeyword($string, &$content)
{
diff --git a/framework/db/sqlite/conditions/InConditionBuilder.php b/framework/db/sqlite/conditions/InConditionBuilder.php
new file mode 100644
index 0000000..9ad8455
--- /dev/null
+++ b/framework/db/sqlite/conditions/InConditionBuilder.php
@@ -0,0 +1,58 @@
+
+ * @since 2.0.14
+ */
+class InConditionBuilder extends \yii\db\conditions\InConditionBuilder
+{
+ /**
+ * {@inheritdoc}
+ * @throws NotSupportedException if `$columns` is an array
+ */
+ protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
+ {
+ if (is_array($columns)) {
+ throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
+ }
+
+ return parent::buildSubqueryInCondition($operator, $columns, $values, $params);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildCompositeInCondition($operator, $columns, $values, &$params)
+ {
+ $quotedColumns = [];
+ foreach ($columns as $i => $column) {
+ $quotedColumns[$i] = strpos($column, '(') === false ? $this->queryBuilder->db->quoteColumnName($column) : $column;
+ }
+ $vss = [];
+ foreach ($values as $value) {
+ $vs = [];
+ foreach ($columns as $i => $column) {
+ if (isset($value[$column])) {
+ $phName = $this->queryBuilder->bindParam($value[$column], $params);
+ $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName;
+ } else {
+ $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL';
+ }
+ }
+ $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')';
+ }
+
+ return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')';
+ }
+}
diff --git a/framework/db/sqlite/conditions/LikeConditionBuilder.php b/framework/db/sqlite/conditions/LikeConditionBuilder.php
new file mode 100644
index 0000000..0aa7d91
--- /dev/null
+++ b/framework/db/sqlite/conditions/LikeConditionBuilder.php
@@ -0,0 +1,19 @@
+sessionCacheLimiter);
+
+ Yii::$app->getSession()->setCacheLimiter($this->sessionCacheLimiter);
}
if ($this->cacheControlHeader !== null) {
diff --git a/framework/filters/PageCache.php b/framework/filters/PageCache.php
index d598b90..89512f7 100644
--- a/framework/filters/PageCache.php
+++ b/framework/filters/PageCache.php
@@ -10,6 +10,8 @@ namespace yii\filters;
use Yii;
use yii\base\Action;
use yii\base\ActionFilter;
+use yii\base\DynamicContentAwareInterface;
+use yii\base\DynamicContentAwareTrait;
use yii\caching\CacheInterface;
use yii\caching\Dependency;
use yii\di\Instance;
@@ -49,8 +51,16 @@ use yii\web\Response;
* @author Sergey Makinen
* @since 2.0
*/
-class PageCache extends ActionFilter
+class PageCache extends ActionFilter implements DynamicContentAwareInterface
{
+ use DynamicContentAwareTrait;
+
+ /**
+ * Page cache version, to detect incompatibilities in cached values when the
+ * data format of the cache changes.
+ */
+ const PAGE_CACHE_VERSION = 1;
+
/**
* @var bool whether the content being cached should be differentiated according to the route.
* A route consists of the requested controller ID and action ID. Defaults to `true`.
@@ -124,13 +134,6 @@ class PageCache extends ActionFilter
* @since 2.0.4
*/
public $cacheHeaders = true;
- /**
- * @var array a list of placeholders for embedding dynamic contents. This property
- * is used internally to implement the content caching feature. Do not modify it.
- * @internal
- * @since 2.0.11
- */
- public $dynamicPlaceholders;
/**
@@ -164,8 +167,8 @@ class PageCache extends ActionFilter
$response = Yii::$app->getResponse();
$data = $this->cache->get($this->calculateCacheKey());
- if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== 1) {
- $this->view->cacheStack[] = $this;
+ if (!is_array($data) || !isset($data['cacheVersion']) || $data['cacheVersion'] !== static::PAGE_CACHE_VERSION) {
+ $this->view->pushDynamicContent($this);
ob_start();
ob_implicit_flush(false);
$response->on(Response::EVENT_AFTER_SEND, [$this, 'cacheResponse']);
@@ -221,13 +224,7 @@ class PageCache extends ActionFilter
}
if (!empty($data['dynamicPlaceholders']) && is_array($data['dynamicPlaceholders'])) {
- if (empty($this->view->cacheStack)) {
- // outermost cache: replace placeholder with dynamic content
- $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders']);
- }
- foreach ($data['dynamicPlaceholders'] as $name => $statements) {
- $this->view->addDynamicPlaceholder($name, $statements);
- }
+ $response->content = $this->updateDynamicContent($response->content, $data['dynamicPlaceholders'], true);
}
$this->afterRestoreResponse(isset($data['cacheData']) ? $data['cacheData'] : null);
}
@@ -238,20 +235,16 @@ class PageCache extends ActionFilter
*/
public function cacheResponse()
{
- array_pop($this->view->cacheStack);
+ $this->view->popDynamicContent();
$beforeCacheResponseResult = $this->beforeCacheResponse();
if ($beforeCacheResponseResult === false) {
- $content = ob_get_clean();
- if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
- $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders);
- }
- echo $content;
+ echo $this->updateDynamicContent(ob_get_clean(), $this->getDynamicPlaceholders());
return;
}
$response = Yii::$app->getResponse();
$data = [
- 'cacheVersion' => 1,
+ 'cacheVersion' => static::PAGE_CACHE_VERSION,
'cacheData' => is_array($beforeCacheResponseResult) ? $beforeCacheResponseResult : null,
'content' => ob_get_clean(),
];
@@ -259,21 +252,14 @@ class PageCache extends ActionFilter
return;
}
- $data['dynamicPlaceholders'] = $this->dynamicPlaceholders;
-
- $data = array_merge($data, [
- 'format' => $response->format,
- 'protocolVersion' => $response->getProtocolVersion(),
- 'statusCode' => $response->getStatusCode(),
- 'reasonPhrase' => $response->getReasonPhrase(),
- ]);
-
+ $data['dynamicPlaceholders'] = $this->getDynamicPlaceholders();
+ foreach (['format', 'version', 'statusCode', 'statusText'] as $name) {
+ $data[$name] = $response->{$name};
+ }
$this->insertResponseCollectionIntoData($response, 'headers', $data);
$this->insertResponseCollectionIntoData($response, 'cookies', $data);
$this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency);
- if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) {
- $data['content'] = $this->updateDynamicContent($data['content'], $this->dynamicPlaceholders);
- }
+ $data['content'] = $this->updateDynamicContent($data['content'], $this->getDynamicPlaceholders());
echo $data['content'];
}
@@ -308,22 +294,6 @@ class PageCache extends ActionFilter
}
/**
- * Replaces placeholders in content by results of evaluated dynamic statements.
- * @param string $content content to be parsed.
- * @param array $placeholders placeholders and their values.
- * @return string final content.
- * @since 2.0.11
- */
- protected function updateDynamicContent($content, $placeholders)
- {
- foreach ($placeholders as $name => $statements) {
- $placeholders[$name] = $this->view->evaluateDynamicContent($statements);
- }
-
- return strtr($content, $placeholders);
- }
-
- /**
* @return array the key used to cache response properties.
* @since 2.0.3
*/
@@ -335,4 +305,12 @@ class PageCache extends ActionFilter
}
return array_merge($key, (array)$this->variations);
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getView()
+ {
+ return $this->view;
+ }
}
diff --git a/framework/filters/auth/HttpBasicAuth.php b/framework/filters/auth/HttpBasicAuth.php
index 1e1abc4..e975dfe 100644
--- a/framework/filters/auth/HttpBasicAuth.php
+++ b/framework/filters/auth/HttpBasicAuth.php
@@ -67,6 +67,7 @@ class HttpBasicAuth extends AuthMethod
* @var callable a PHP callable that will authenticate the user with the HTTP basic auth information.
* The callable receives a username and a password as its parameters. It should return an identity object
* that matches the username and password. Null should be returned if there is no such identity.
+ * The callable will be called only if current user is not authenticated.
*
* The following code is a typical implementation of this callable:
*
@@ -95,11 +96,12 @@ class HttpBasicAuth extends AuthMethod
if ($this->auth) {
if ($username !== null || $password !== null) {
- $identity = call_user_func($this->auth, $username, $password);
- if ($identity !== null) {
- $user->switchIdentity($identity);
- } else {
+ $identity = $user->getIdentity() ?: call_user_func($this->auth, $username, $password);
+
+ if ($identity === null) {
$this->handleFailure($response);
+ } elseif ($user->getIdentity(false) !== $identity) {
+ $user->switchIdentity($identity);
}
return $identity;
diff --git a/framework/filters/auth/HttpBearerAuth.php b/framework/filters/auth/HttpBearerAuth.php
index 7a3b368..dd34abb 100644
--- a/framework/filters/auth/HttpBearerAuth.php
+++ b/framework/filters/auth/HttpBearerAuth.php
@@ -37,8 +37,8 @@ class HttpBearerAuth extends HttpHeaderAuth
*/
public $pattern = '/^Bearer\s+(.*?)$/';
/**
- * @var string the HTTP authentication realm
- */
+ * @var string the HTTP authentication realm
+ */
public $realm = 'api';
diff --git a/framework/grid/DataColumn.php b/framework/grid/DataColumn.php
index 60338f0..2953430 100644
--- a/framework/grid/DataColumn.php
+++ b/framework/grid/DataColumn.php
@@ -207,8 +207,8 @@ class DataColumn extends Column
if ($this->format === 'boolean') {
$options = array_merge(['prompt' => ''], $filterOptions);
return Html::activeDropDownList($model, $this->attribute, [
- $this->grid->formatter->booleanFormat[0],
- $this->grid->formatter->booleanFormat[1],
+ 1 => $this->grid->formatter->booleanFormat[1],
+ 0 => $this->grid->formatter->booleanFormat[0],
], $options) . $error;
}
diff --git a/framework/grid/GridView.php b/framework/grid/GridView.php
index 7178871..8a44123 100644
--- a/framework/grid/GridView.php
+++ b/framework/grid/GridView.php
@@ -125,11 +125,11 @@ class GridView extends BaseListView
* @var bool whether to show the footer section of the grid table.
*/
public $showFooter = false;
- /**
- * @var bool whether to place footer after body in DOM if $showFooter is true
+ /**
+ * @var bool whether to place footer after body in DOM if $showFooter is true
* @since 2.0.14
- */
- public $placeFooterAfterBody = false;
+ */
+ public $placeFooterAfterBody = false;
/**
* @var bool whether to show the grid view if [[dataProvider]] returns no data.
*/
diff --git a/framework/helpers/BaseFileHelper.php b/framework/helpers/BaseFileHelper.php
index 2eddf3a..a4d58d0 100644
--- a/framework/helpers/BaseFileHelper.php
+++ b/framework/helpers/BaseFileHelper.php
@@ -9,8 +9,8 @@ namespace yii\helpers;
use Yii;
use yii\base\ErrorException;
-use yii\base\InvalidConfigException;
use yii\base\InvalidArgumentException;
+use yii\base\InvalidConfigException;
/**
* BaseFileHelper provides concrete implementation for [[FileHelper]].
@@ -33,7 +33,6 @@ class BaseFileHelper
* @var string the path (or alias) of a PHP file containing MIME type information.
*/
public static $mimeMagicFile = '@yii/helpers/mimeTypes.php';
-
/**
* @var string the path (or alias) of a PHP file containing MIME aliases.
* @since 2.0.14
@@ -62,7 +61,11 @@ class BaseFileHelper
return $path;
}
// the path may contain ".", ".." or double slashes, need to clean them up
- $parts = [];
+ if (strpos($path, "{$ds}{$ds}") === 0 && $ds == '\\') {
+ $parts = [$ds];
+ } else {
+ $parts = [];
+ }
foreach (explode($ds, $path) as $part) {
if ($part === '..' && !empty($parts) && end($parts) !== '..') {
array_pop($parts);
@@ -288,7 +291,7 @@ class BaseFileHelper
* that do not contain files. This affects directories that do not contain files initially as well as directories that
* do not contain files at the target destination because files have been filtered via `only` or `except`.
* Defaults to true. This option is available since version 2.0.12. Before 2.0.12 empty directories are always copied.
- * @throws \yii\base\InvalidArgumentException if unable to open directory
+ * @throws InvalidArgumentException if unable to open directory
*/
public static function copyDirectory($src, $dst, $options = [])
{
@@ -413,7 +416,7 @@ class BaseFileHelper
} catch (ErrorException $e) {
// last resort measure for Windows
$lines = [];
- exec("DEL /F/Q \"$path\"", $lines, $deleteError);
+ exec('DEL /F/Q ' . escapeshellarg($path), $lines, $deleteError);
return $deleteError !== 0;
}
}
@@ -455,20 +458,10 @@ class BaseFileHelper
*/
public static function findFiles($dir, $options = [])
{
- if (!is_dir($dir)) {
- throw new InvalidArgumentException("The dir argument must be a directory: $dir");
- }
- $dir = rtrim($dir, DIRECTORY_SEPARATOR);
- if (!isset($options['basePath'])) {
- // this should be done only once
- $options['basePath'] = realpath($dir);
- $options = static::normalizeOptions($options);
- }
+ $dir = self::clearDir($dir);
+ $options = self::setBasePath($dir, $options);
$list = [];
- $handle = opendir($dir);
- if ($handle === false) {
- throw new InvalidArgumentException("Unable to open directory: $dir");
- }
+ $handle = self::openDir($dir);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
@@ -488,6 +481,83 @@ class BaseFileHelper
}
/**
+ * Returns the directories found under the specified directory and subdirectories.
+ * @param string $dir the directory under which the files will be looked for.
+ * @param array $options options for directory searching. Valid options are:
+ *
+ * - `filter`: callback, a PHP callback that is called for each directory or file.
+ * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
+ * The callback can return one of the following values:
+ *
+ * * `true`: the directory will be returned
+ * * `false`: the directory will NOT be returned
+ *
+ * - `recursive`: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
+ * @return array directories found under the directory, in no particular order. Ordering depends on the files system used.
+ * @throws InvalidArgumentException if the dir is invalid.
+ * @since 2.0.14
+ */
+ public static function findDirectories($dir, $options = [])
+ {
+ $dir = self::clearDir($dir);
+ $options = self::setBasePath($dir, $options);
+ $list = [];
+ $handle = self::openDir($dir);
+ while (($file = readdir($handle)) !== false) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+ $path = $dir . DIRECTORY_SEPARATOR . $file;
+ if (is_dir($path) && static::filterPath($path, $options)) {
+ $list[] = $path;
+ if (!isset($options['recursive']) || $options['recursive']) {
+ $list = array_merge($list, static::findDirectories($path, $options));
+ }
+ }
+ }
+ closedir($handle);
+
+ return $list;
+ }
+
+ /*
+ * @param string $dir
+ */
+ private static function setBasePath($dir, $options)
+ {
+ if (!isset($options['basePath'])) {
+ // this should be done only once
+ $options['basePath'] = realpath($dir);
+ $options = static::normalizeOptions($options);
+ }
+
+ return $options;
+ }
+
+ /*
+ * @param string $dir
+ */
+ private static function openDir($dir)
+ {
+ $handle = opendir($dir);
+ if ($handle === false) {
+ throw new InvalidArgumentException("Unable to open directory: $dir");
+ }
+ return $handle;
+ }
+
+ /*
+ * @param string $dir
+ */
+ private static function clearDir($dir)
+ {
+ if (!is_dir($dir)) {
+ throw new InvalidArgumentException("The dir argument must be a directory: $dir");
+ }
+ return rtrim($dir, DIRECTORY_SEPARATOR);
+ }
+
+ /**
* Checks if the given file path satisfies the filtering options.
* @param string $path the path of the file or directory to be checked
* @param array $options the filtering options. See [[findFiles()]] for explanations of
@@ -676,8 +746,7 @@ class BaseFileHelper
$exclude = self::parseExcludePattern($exclude, false);
}
if (!isset($exclude['pattern']) || !isset($exclude['flags']) || !isset($exclude['firstWildcard'])) {
- throw new InvalidArgumentException('If exclude/include pattern is an array it must contain the '
- . 'pattern, flags and firstWildcard keys.');
+ throw new InvalidArgumentException('If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.');
}
if ($exclude['flags'] & self::PATTERN_MUSTBEDIR && !is_dir($path)) {
continue;
@@ -702,7 +771,7 @@ class BaseFileHelper
* Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
* @param string $pattern
* @param bool $caseSensitive
- * @throws \yii\base\InvalidArgumentException
+ * @throws InvalidArgumentException
* @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
*/
private static function parseExcludePattern($pattern, $caseSensitive)
diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php
index 75954fc..a48c68f 100644
--- a/framework/helpers/BaseHtml.php
+++ b/framework/helpers/BaseHtml.php
@@ -939,6 +939,9 @@ class BaseHtml
if (substr($name, -2) !== '[]') {
$name .= '[]';
}
+ if (ArrayHelper::isTraversable($selection)) {
+ $selection = array_map('strval', (array)$selection);
+ }
$formatter = ArrayHelper::remove($options, 'item');
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
@@ -951,7 +954,7 @@ class BaseHtml
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!ArrayHelper::isTraversable($selection) && !strcmp($value, $selection)
- || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn($value, $selection));
+ || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
@@ -1016,6 +1019,10 @@ class BaseHtml
*/
public static function radioList($name, $selection = null, $items = [], $options = [])
{
+ if (ArrayHelper::isTraversable($selection)) {
+ $selection = array_map('strval', (array)$selection);
+ }
+
$formatter = ArrayHelper::remove($options, 'item');
$itemOptions = ArrayHelper::remove($options, 'itemOptions', []);
$encode = ArrayHelper::remove($options, 'encode', true);
@@ -1030,7 +1037,7 @@ class BaseHtml
foreach ($items as $value => $label) {
$checked = $selection !== null &&
(!ArrayHelper::isTraversable($selection) && !strcmp($value, $selection)
- || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn($value, $selection));
+ || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$value, $selection));
if ($formatter !== null) {
$lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value);
} else {
@@ -1438,7 +1445,7 @@ class BaseHtml
* @param array $options the tag options in terms of name-value pairs. These will be rendered as
* the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]].
* See [[renderTagAttributes()]] for details on how attributes are being rendered.
- * If `hiddenOptions` parameter which is another set of HTML options array is defined, it will be extracted
+ * If `hiddenOptions` parameter which is another set of HTML options array is defined, it will be extracted
* from `$options` to be used for the hidden input.
* @return string the generated input tag
*/
@@ -1807,6 +1814,10 @@ class BaseHtml
*/
public static function renderSelectOptions($selection, $items, &$tagOptions = [])
{
+ if (ArrayHelper::isTraversable($selection)) {
+ $selection = array_map('strval', (array)$selection);
+ }
+
$lines = [];
$encodeSpaces = ArrayHelper::remove($tagOptions, 'encodeSpaces', false);
$encode = ArrayHelper::remove($tagOptions, 'encode', true);
@@ -1846,7 +1857,7 @@ class BaseHtml
if (!array_key_exists('selected', $attrs)) {
$attrs['selected'] = $selection !== null &&
(!ArrayHelper::isTraversable($selection) && !strcmp($key, $selection)
- || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn($key, $selection));
+ || ArrayHelper::isTraversable($selection) && ArrayHelper::isIn((string)$key, $selection));
}
$text = $encode ? static::encode($value) : $value;
if ($encodeSpaces) {
diff --git a/framework/helpers/BaseIpHelper.php b/framework/helpers/BaseIpHelper.php
index 3a4c35f..5af851b 100644
--- a/framework/helpers/BaseIpHelper.php
+++ b/framework/helpers/BaseIpHelper.php
@@ -1,4 +1,9 @@
controller === null) {
- throw new InvalidArgumentException("Unable to resolve the relative route: $route. No active controller "
- . "is available.");
+ throw new InvalidArgumentException("Unable to resolve the relative route: $route. No active controller is available.");
}
if (strpos($route, '/') === false) {
diff --git a/framework/helpers/IpHelper.php b/framework/helpers/IpHelper.php
index 8aedef4..1831659 100644
--- a/framework/helpers/IpHelper.php
+++ b/framework/helpers/IpHelper.php
@@ -1,4 +1,9 @@
nullDisplay;
}
+
return $value;
}
@@ -471,6 +472,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return Html::encode($value);
}
@@ -484,6 +486,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return nl2br(Html::encode($value));
}
@@ -499,6 +502,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return str_replace('', '', '
' . preg_replace('/\R{2,}/u', "
\n
", Html::encode($value)) . '
');
}
@@ -515,6 +519,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return HtmlPurifier::process($value, $config);
}
@@ -529,6 +534,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return Html::mailto(Html::encode($value), $value, $options);
}
@@ -543,6 +549,7 @@ class Formatter extends Component
if ($value === null) {
return $this->nullDisplay;
}
+
return Html::img($value, $options);
}
@@ -1056,8 +1063,7 @@ class Formatter extends Component
$f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
$f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
- throw new InvalidArgumentException('Formatting integer value failed: ' . $f->getErrorCode() . ' '
- . $f->getErrorMessage());
+ throw new InvalidArgumentException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
}
return $result;
@@ -1097,8 +1103,7 @@ class Formatter extends Component
if ($this->_intlLoaded) {
$f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
if (($result = $f->format($value)) === false) {
- throw new InvalidArgumentException('Formatting decimal value failed: ' . $f->getErrorCode() . ' '
- . $f->getErrorMessage());
+ throw new InvalidArgumentException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
}
return $result;
@@ -1138,8 +1143,7 @@ class Formatter extends Component
if ($this->_intlLoaded) {
$f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
if (($result = $f->format($value)) === false) {
- throw new InvalidArgumentException('Formatting percent value failed: ' . $f->getErrorCode() . ' '
- . $f->getErrorMessage());
+ throw new InvalidArgumentException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
}
return $result;
diff --git a/framework/i18n/Locale.php b/framework/i18n/Locale.php
new file mode 100644
index 0000000..c2670a3
--- /dev/null
+++ b/framework/i18n/Locale.php
@@ -0,0 +1,64 @@
+locale === null) {
+ $this->locale = Yii::$app->language;
+ }
+ }
+
+ /**
+ * Returns a currency symbol
+ *
+ * @param string $currencyCode the 3-letter ISO 4217 currency code to get symbol for. If null,
+ * method will attempt using currency code from [[locale]].
+ * @return string
+ */
+ public function getCurrencySymbol($currencyCode = null)
+ {
+ $locale = $this->locale;
+
+ if ($currencyCode !== null) {
+ $locale .= '@currency=' . $currencyCode;
+ }
+
+ $formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+ return $formatter->getSymbol(\NumberFormatter::CURRENCY_SYMBOL);
+ }
+}
diff --git a/framework/jquery/assets/yii.activeForm.js b/framework/jquery/assets/yii.activeForm.js
index e9945dc..5cda2a5 100644
--- a/framework/jquery/assets/yii.activeForm.js
+++ b/framework/jquery/assets/yii.activeForm.js
@@ -134,7 +134,9 @@
// whether to scroll to first visible error after validation.
scrollToError: true,
// offset in pixels that should be added when scrolling to the first error.
- scrollToErrorOffset: 0
+ scrollToErrorOffset: 0,
+ // where to add validation class: container or input
+ validationStateOn: 'container'
};
// NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well
@@ -441,8 +443,11 @@
// Without setTimeout() we would get the input values that are not reset yet.
this.value = getValue($form, this);
this.status = 0;
- var $container = $form.find(this.container);
- $container.removeClass(
+ var $container = $form.find(this.container),
+ $input = findInput($form, attribute),
+ $errorElement = data.settings.validationStateOn === 'input' ? $input : $container;
+
+ $errorElement.removeClass(
data.settings.validatingCssClass + ' ' +
data.settings.errorCssClass + ' ' +
data.settings.successCssClass
@@ -711,17 +716,20 @@
var $container = $form.find(attribute.container);
var $error = $container.find(attribute.error);
updateAriaInvalid($form, attribute, hasError);
+
+ var $errorElement = data.settings.validationStateOn === 'input' ? $input : $container;
+
if (hasError) {
if (attribute.encodeError) {
$error.text(messages[attribute.id][0]);
} else {
$error.html(messages[attribute.id][0]);
}
- $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
+ $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass)
.addClass(data.settings.errorCssClass);
} else {
$error.empty();
- $container.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
+ $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ')
.addClass(data.settings.successCssClass);
}
attribute.value = getValue($form, attribute);
diff --git a/framework/log/DbTarget.php b/framework/log/DbTarget.php
index 2025fa6..0f1e676 100644
--- a/framework/log/DbTarget.php
+++ b/framework/log/DbTarget.php
@@ -10,7 +10,9 @@ namespace yii\log;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\Connection;
+use yii\db\Exception;
use yii\di\Instance;
+use yii\helpers\VarDumper;
/**
* DbTarget stores log messages in a database table.
@@ -56,6 +58,9 @@ class DbTarget extends Target
/**
* Stores log messages to DB.
+ * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
+ * @throws Exception
+ * @throws LogRuntimeException
*/
public function export()
{
@@ -71,13 +76,24 @@ class DbTarget extends Target
$command = $this->db->createCommand($sql);
foreach ($this->messages as $message) {
[$level, $text, $context] = $message;
- $command->bindValues([
- ':level' => $level,
- ':category' => $context['category'],
- ':log_time' => $context['time'],
- ':prefix' => $this->getMessagePrefix($message),
- ':message' => $text,
- ])->execute();
+ if (!is_string($text)) {
+ // exceptions may not be serializable if in the call stack somewhere is a Closure
+ if ($text instanceof \Throwable || $text instanceof \Exception) {
+ $text = (string) $text;
+ } else {
+ $text = VarDumper::export($text);
+ }
+ }
+ if ($command->bindValues([
+ ':level' => $level,
+ ':category' => $category,
+ ':log_time' => $timestamp,
+ ':prefix' => $this->getMessagePrefix($message),
+ ':message' => $text,
+ ])->execute() > 0) {
+ continue;
+ }
+ throw new LogRuntimeException('Unable to export log through database!');
}
}
}
diff --git a/framework/log/EmailTarget.php b/framework/log/EmailTarget.php
index 5cbb544..d2291c8 100644
--- a/framework/log/EmailTarget.php
+++ b/framework/log/EmailTarget.php
@@ -72,6 +72,8 @@ class EmailTarget extends Target
/**
* Sends log messages to specified email addresses.
+ * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
+ * @throws LogRuntimeException
*/
public function export()
{
@@ -82,7 +84,10 @@ class EmailTarget extends Target
}
$messages = array_map([$this, 'formatMessage'], $this->messages);
$body = wordwrap(implode("\n", $messages), 70);
- $this->composeMessage($body)->send($this->mailer);
+ $message = $this->composeMessage($body);
+ if (!$message->send($this->mailer)) {
+ throw new LogRuntimeException('Unable to export log through email!');
+ }
}
/**
diff --git a/framework/log/FileTarget.php b/framework/log/FileTarget.php
index 427a08c..4f077e5 100644
--- a/framework/log/FileTarget.php
+++ b/framework/log/FileTarget.php
@@ -85,10 +85,6 @@ class FileTarget extends Target
} else {
$this->logFile = Yii::getAlias($this->logFile);
}
- $logPath = dirname($this->logFile);
- if (!is_dir($logPath)) {
- FileHelper::createDirectory($logPath, $this->dirMode, true);
- }
if ($this->maxLogFiles < 1) {
$this->maxLogFiles = 1;
}
@@ -99,10 +95,15 @@ class FileTarget extends Target
/**
* Writes log messages to a file.
+ * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
* @throws InvalidConfigException if unable to open the log file for writing
+ * @throws LogRuntimeException if unable to write complete log to file
*/
public function export()
{
+ $logPath = dirname($this->logFile);
+ FileHelper::createDirectory($logPath, $this->dirMode, true);
+
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
if (($fp = @fopen($this->logFile, 'a')) === false) {
throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
@@ -117,9 +118,25 @@ class FileTarget extends Target
$this->rotateFiles();
@flock($fp, LOCK_UN);
@fclose($fp);
- @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX);
+ $writeResult = @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX);
+ if ($writeResult === false) {
+ $error = error_get_last();
+ throw new LogRuntimeException("Unable to export log through file!: {$error['message']}");
+ }
+ $textSize = strlen($text);
+ if ($writeResult < $textSize) {
+ throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes.");
+ }
} else {
- @fwrite($fp, $text);
+ $writeResult = @fwrite($fp, $text);
+ if ($writeResult === false) {
+ $error = error_get_last();
+ throw new LogRuntimeException("Unable to export log through file!: {$error['message']}");
+ }
+ $textSize = strlen($text);
+ if ($writeResult < $textSize) {
+ throw new LogRuntimeException("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes.");
+ }
@flock($fp, LOCK_UN);
@fclose($fp);
}
diff --git a/framework/log/LogRuntimeException.php b/framework/log/LogRuntimeException.php
new file mode 100644
index 0000000..888e2f5
--- /dev/null
+++ b/framework/log/LogRuntimeException.php
@@ -0,0 +1,25 @@
+
+ * @since 2.0.14
+ */
+class LogRuntimeException extends \yii\base\Exception
+{
+ /**
+ * @return string the user-friendly name of this exception
+ */
+ public function getName()
+ {
+ return 'Log Runtime';
+ }
+}
diff --git a/framework/log/SyslogTarget.php b/framework/log/SyslogTarget.php
index 4e16f1a..7f044a4 100644
--- a/framework/log/SyslogTarget.php
+++ b/framework/log/SyslogTarget.php
@@ -62,12 +62,16 @@ class SyslogTarget extends Target
/**
* Writes log messages to syslog.
+ * Starting from version 2.0.14, this method throws LogRuntimeException in case the log can not be exported.
+ * @throws LogRuntimeException
*/
public function export()
{
openlog($this->identity, $this->options, $this->facility);
foreach ($this->messages as $message) {
- syslog($this->_syslogLevels[$message[0]], $this->formatMessage($message));
+ if (syslog($this->_syslogLevels[$message[0]], $this->formatMessage($message)) === false) {
+ throw new LogRuntimeException('Unable to export log through system log!');
+ }
}
closelog();
}
diff --git a/framework/mail/MessageInterface.php b/framework/mail/MessageInterface.php
index fd584cc..1b79b96 100644
--- a/framework/mail/MessageInterface.php
+++ b/framework/mail/MessageInterface.php
@@ -47,7 +47,7 @@ interface MessageInterface
/**
* Returns the message sender.
- * @return string the sender
+ * @return string|array the sender
*/
public function getFrom();
@@ -63,7 +63,7 @@ interface MessageInterface
/**
* Returns the message recipient(s).
- * @return array the message recipients
+ * @return string|array the message recipients
*/
public function getTo();
@@ -79,7 +79,7 @@ interface MessageInterface
/**
* Returns the reply-to address of this message.
- * @return string the reply-to address of this message.
+ * @return string|array the reply-to address of this message.
*/
public function getReplyTo();
@@ -95,7 +95,7 @@ interface MessageInterface
/**
* Returns the Cc (additional copy receiver) addresses of this message.
- * @return array the Cc (additional copy receiver) addresses of this message.
+ * @return string|array the Cc (additional copy receiver) addresses of this message.
*/
public function getCc();
@@ -111,7 +111,7 @@ interface MessageInterface
/**
* Returns the Bcc (hidden copy receiver) addresses of this message.
- * @return array the Bcc (hidden copy receiver) addresses of this message.
+ * @return string|array the Bcc (hidden copy receiver) addresses of this message.
*/
public function getBcc();
diff --git a/framework/messages/sk/yii.php b/framework/messages/sk/yii.php
index 283b66e..67b4f67 100644
--- a/framework/messages/sk/yii.php
+++ b/framework/messages/sk/yii.php
@@ -23,16 +23,13 @@
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
- '"{attribute}" does not support operator "{operator}".' => '"{attribute}" nepodporuje operátor "{operator}".',
- 'Condition for "{attribute}" should be either a value or valid operator specification.' => 'Podmienkou pre "{attribute}" by mala byť hodnota alebo platná špecifikácia operátora.',
- 'Operator "{operator}" must be used with a search attribute.' => 'Operátor "{operator}" musí byť použitý s atribútom vyhľadávania.',
- 'Operator "{operator}" requires multiple operands.' => 'Operátor "{operator}" vyžaduje viac operandov.',
- 'The format of {filter} is invalid.' => 'Format {filter} je neplatný.',
- 'Unknown filter attribute "{attribute}"' => 'Neznámy atribút filtra "{attribute}"',
+ 'You should upload at least {limit, number} {limit, plural, one{file} other{files}}.' => 'Je potrebné nahrať aspoň {limit, number} {limit, plural, =1{súbor} =2{súbory} =3{súbory} =4{súbory} other{súborov}}.',
' and ' => ' a ',
+ '"{attribute}" does not support operator "{operator}".' => '"{attribute}" nepodporuje operátor "{operator}".',
'(not set)' => '(nie je nastavené)',
'An internal server error occurred.' => 'Vyskytla sa interná chyba servera.',
'Are you sure you want to delete this item?' => 'Skutočne chcete odstrániť tento záznam?',
+ 'Condition for "{attribute}" should be either a value or valid operator specification.' => 'Podmienkou pre "{attribute}" by mala byť hodnota alebo platná špecifikácia operátora.',
'Delete' => 'Zmazať',
'Error' => 'Chyba',
'File upload failed.' => 'Súbor sa nepodarilo nahrať.',
@@ -45,6 +42,8 @@ return [
'No results found.' => 'Neboli nájdené žiadne záznamy.',
'Only files with these MIME types are allowed: {mimeTypes}.' => 'Povolené sú len súbory nasledovných MIME typov: {mimeTypes}.',
'Only files with these extensions are allowed: {extensions}.' => 'Povolené sú len súbory s nasledovnými príponami: {extensions}.',
+ 'Operator "{operator}" must be used with a search attribute.' => 'Operátor "{operator}" musí byť použitý s atribútom vyhľadávania.',
+ 'Operator "{operator}" requires multiple operands.' => 'Operátor "{operator}" vyžaduje viac operandov.',
'Page not found.' => 'Stránka nebola nájdená.',
'Please fix the following errors:' => 'Opravte prosím nasledujúce chyby:',
'Please upload a file.' => 'Nahrajte prosím súbor.',
@@ -54,28 +53,30 @@ return [
'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'Súbor "{file}" je príliš veľký. Veľkosť súboru nesmie byť viac ako {formattedLimit}.',
'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'Súbor "{file}" je príliš malý. Veľkosť súboru nesmie byť menej ako {formattedLimit}.',
'The format of {attribute} is invalid.' => 'Formát atribútu {attribute} je neplatný.',
- 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš veľký. Výška nesmie presiahnuť {limit, number} {limit, plural, one{pixel} other{pixlov}}.',
- 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš veľký. Šírka nesmie presiahnuť {limit, number} {limit, plural, one{pixel} other{pixlov}}.',
- 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš malý. Výška nesmie byť menšia ako {limit, number} {limit, plural, one{pixel} other{pixlov}}.',
- 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš malý. Šírka nesmie byť menšia ako {limit, number} {limit, plural, one{pixel} other{pixlov}}.',
+ 'The format of {filter} is invalid.' => 'Format {filter} je neplatný.',
+ 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš veľký. Výška nesmie presiahnuť {limit, number} {limit, plural, =1{pixel} =2{pixle} =3{pixle} =4{pixle} other{pixlov}}.',
+ 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš veľký. Šírka nesmie presiahnuť {limit, number} {limit, plural, =1{pixel} =2{pixle} =3{pixle} =4{pixle} other{pixlov}}.',
+ 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš malý. Výška nesmie byť menšia ako {limit, number} {limit, plural, =1{pixel} =2{pixle} =3{pixle} =4{pixle} other{pixlov}}.',
+ 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Obrázok "{file}" je príliš malý. Šírka nesmie byť menšia ako {limit, number} {limit, plural, =1{pixel} =2{pixle} =3{pixle} =4{pixle} other{pixlov}}.',
'The requested view "{name}" was not found.' => 'Požadovaná stránka "{name}" nebola nájdená.',
'The verification code is incorrect.' => 'Kód pre overenie je neplatný.',
- 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Celkovo {count, number} {count, plural, one{záznam} other{záznamov}}.',
+ 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Celkovo {count, number} {count, plural, =1{záznam} =2{záznamy} =3{záznamy} =4{záznamy} other{záznamov}}.',
'Unable to verify your data submission.' => 'Nebolo možné preveriť odoslané údaje.',
'Unknown alias: -{name}' => 'Neznámy alias: -{name}',
+ 'Unknown filter attribute "{attribute}"' => 'Neznámy atribút filtra "{attribute}"',
'Unknown option: --{name}' => 'Neznáme nastavenie: --{name}',
'Update' => 'Upraviť',
'View' => 'Náhľad',
'Yes' => 'Áno',
'Yii Framework' => 'Yii Framework',
'You are not allowed to perform this action.' => 'Nemáte oprávnenie pre požadovanú akciu.',
- 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Nahrať môžete najviac {limit, number} {limit, plural, one{súbor} other{súborov}}.',
- 'in {delta, plural, =1{a day} other{# days}}' => 'o {delta, plural, =1{deň} other{# dni}}',
- 'in {delta, plural, =1{a minute} other{# minutes}}' => 'o {delta, plural, =1{minútu} other{# minút}}',
- 'in {delta, plural, =1{a month} other{# months}}' => 'o {delta, plural, =1{mesiac} other{# mesiacov}}',
- 'in {delta, plural, =1{a second} other{# seconds}}' => 'o {delta, plural, =1{sekundu} other{# sekúnd}}',
- 'in {delta, plural, =1{a year} other{# years}}' => 'o {delta, plural, =1{rok} other{# rokov}}',
- 'in {delta, plural, =1{an hour} other{# hours}}' => 'o {delta, plural, =1{hodinu} other{# hodín}}',
+ 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Nahrať môžete najviac {limit, number} {limit, plural, =1{súbor} =2{súbory} =3{súbory} =4{súbory} other{súborov}}.',
+ 'in {delta, plural, =1{a day} other{# days}}' => 'o {delta, plural, =1{deň} =2{dni} =3{dni} =4{dni} other{# dní}}',
+ 'in {delta, plural, =1{a minute} other{# minutes}}' => 'o {delta, plural, =1{minútu} =2{minúty} =3{minúty} =4{minúty} other{# minút}}',
+ 'in {delta, plural, =1{a month} other{# months}}' => 'o {delta, plural, =1{mesiac} =2{mesiace} =3{mesiace} =4{mesiace} other{# mesiacov}}',
+ 'in {delta, plural, =1{a second} other{# seconds}}' => 'o {delta, plural, =1{sekundu} =2{sekundy} =3{sekundy} =4{sekundy} other{# sekúnd}}',
+ 'in {delta, plural, =1{a year} other{# years}}' => 'o {delta, plural, =1{rok} =2{roky} =3{roky} =4{roky} other{# rokov}}',
+ 'in {delta, plural, =1{an hour} other{# hours}}' => 'o {delta, plural, =1{hodinu} =2{hodiny} =3{hodiny} =4{hodiny} other{# hodín}}',
'just now' => 'práve teraz',
'the input value' => 'vstupná hodnota',
'{attribute} "{value}" has already been taken.' => '{attribute} "{value}" je už použité.',
@@ -103,9 +104,9 @@ return [
'{attribute} must not be an IPv4 address.' => '{attribute} nesmie byť IPv4 adresa.',
'{attribute} must not be an IPv6 address.' => '{attribute} nesmie byť IPv6 adresa.',
'{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} sa nesmie rovnať "{compareValueOrAttribute}".',
- '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} musí obsahovať aspoň {min, number} {min, plural, one{znak} other{znakov}}.',
- '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} môže obsahovať najviac {max, number} {max, plural, one{znak} other{znakov}}.',
- '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} musí obsahovať {length, number} {length, plural, one{znak} other{znakov}}.',
+ '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} musí obsahovať aspoň {min, number} {min, plural, =1{znak} =2{znaky} =3{znaky} =4{znaky} other{znakov}}.',
+ '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} môže obsahovať najviac {max, number} {max, plural, =1{znak} =2{znaky} =3{znaky} =4{znaky} other{znakov}}.',
+ '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} musí obsahovať {length, number} {length, plural, =1{znak} =2{znaky} =3{znaky} =4{znaky} other{znakov}}.',
'{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 deň} =2{2 dni} =3{3 dni} =4{4 dni} other{# dní}}',
'{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 hodina} =2{2 hodiny} =3{3 hodiny} =4{4 hodiny} other{# hodín}}',
'{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 minúta} =2{2 minúty} =3{3 minúty} =4{4 minúty} other{# minút}}',
diff --git a/framework/rbac/BaseManager.php b/framework/rbac/BaseManager.php
index 7bafc89..d525d95 100644
--- a/framework/rbac/BaseManager.php
+++ b/framework/rbac/BaseManager.php
@@ -8,8 +8,9 @@
namespace yii\rbac;
use yii\base\Component;
-use yii\base\InvalidConfigException;
use yii\base\InvalidArgumentException;
+use yii\base\InvalidConfigException;
+use yii\base\InvalidValueException;
/**
* BaseManager is a base class implementing [[ManagerInterface]] for RBAC management.
@@ -18,6 +19,8 @@ use yii\base\InvalidArgumentException;
*
* @property Role[] $defaultRoleInstances Default roles. The array is indexed by the role names. This property
* is read-only.
+ * @property array $defaultRoles Default roles. Note that the type of this property differs in getter and
+ * setter. See [[getDefaultRoles()]] and [[setDefaultRoles()]] for details.
*
* @author Qiang Xue
* @since 2.0
@@ -197,17 +200,19 @@ abstract class BaseManager extends Component implements ManagerInterface
/**
* Set default roles
- * @param array|\Closure $roles either array of roles or a callable returning it
+ * @param string[]|\Closure $roles either array of roles or a callable returning it
+ * @throws InvalidArgumentException when $roles is neither array nor Closure
+ * @throws InvalidValueException when Closure return is not an array
* @since 2.0.14
*/
public function setDefaultRoles($roles)
{
if (is_array($roles)) {
$this->defaultRoles = $roles;
- } elseif (is_callable($roles)) {
- $roles = $roles();
+ } elseif ($roles instanceof \Closure) {
+ $roles = call_user_func($roles);
if (!is_array($roles)) {
- throw new InvalidArgumentException('Default roles closure must return an array');
+ throw new InvalidValueException('Default roles closure must return an array');
}
$this->defaultRoles = $roles;
} else {
@@ -217,7 +222,7 @@ abstract class BaseManager extends Component implements ManagerInterface
/**
* Get default roles
- * @return array default roles
+ * @return string[] default roles
* @since 2.0.14
*/
public function getDefaultRoles()
diff --git a/framework/test/ActiveFixture.php b/framework/test/ActiveFixture.php
index 87eff0f..b89e732 100644
--- a/framework/test/ActiveFixture.php
+++ b/framework/test/ActiveFixture.php
@@ -7,7 +7,6 @@
namespace yii\test;
-use Yii;
use yii\base\InvalidConfigException;
use yii\db\TableSchema;
@@ -95,10 +94,15 @@ class ActiveFixture extends BaseActiveFixture
protected function getData()
{
if ($this->dataFile === null) {
- $class = new \ReflectionClass($this);
- $dataFile = dirname($class->getFileName()) . '/data/' . $this->getTableSchema()->fullName . '.php';
- return is_file($dataFile) ? require $dataFile : [];
+ if ($this->dataDirectory !== null) {
+ $dataFile = $this->getTableSchema()->fullName . '.php';
+ } else {
+ $class = new \ReflectionClass($this);
+ $dataFile = dirname($class->getFileName()) . '/data/' . $this->getTableSchema()->fullName . '.php';
+ }
+
+ return $this->loadData($dataFile);
}
return parent::getData();
diff --git a/framework/test/ArrayFixture.php b/framework/test/ArrayFixture.php
index 3ce967d..ea0c000 100644
--- a/framework/test/ArrayFixture.php
+++ b/framework/test/ArrayFixture.php
@@ -7,7 +7,6 @@
namespace yii\test;
-use Yii;
use yii\base\ArrayAccessTrait;
use yii\base\InvalidConfigException;
@@ -22,16 +21,12 @@ use yii\base\InvalidConfigException;
class ArrayFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, \Countable
{
use ArrayAccessTrait;
+ use FileFixtureTrait;
/**
* @var array the data rows. Each array element represents one row of data (column name => column value).
*/
public $data = [];
- /**
- * @var string|bool the file path or [path alias](guide:concept-aliases) of the data file that contains the fixture data
- * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data.
- */
- public $dataFile;
/**
@@ -56,15 +51,7 @@ class ArrayFixture extends Fixture implements \IteratorAggregate, \ArrayAccess,
*/
protected function getData()
{
- if ($this->dataFile === false || $this->dataFile === null) {
- return [];
- }
- $dataFile = Yii::getAlias($this->dataFile);
- if (is_file($dataFile)) {
- return require $dataFile;
- }
-
- throw new InvalidConfigException("Fixture data file does not exist: {$this->dataFile}");
+ return $this->loadData($this->dataFile);
}
/**
diff --git a/framework/test/BaseActiveFixture.php b/framework/test/BaseActiveFixture.php
index 1e7a778..100e770 100644
--- a/framework/test/BaseActiveFixture.php
+++ b/framework/test/BaseActiveFixture.php
@@ -7,7 +7,6 @@
namespace yii\test;
-use Yii;
use yii\base\ArrayAccessTrait;
use yii\base\InvalidConfigException;
@@ -22,6 +21,7 @@ use yii\base\InvalidConfigException;
abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate, \ArrayAccess, \Countable
{
use ArrayAccessTrait;
+ use FileFixtureTrait;
/**
* @var string the AR model class associated with this fixture.
@@ -31,11 +31,6 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate
* @var array the data rows. Each array element represents one row of data (column name => column value).
*/
public $data = [];
- /**
- * @var string|bool the file path or [path alias](guide:concept-aliases) of the data file that contains the fixture data
- * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data.
- */
- public $dataFile;
/**
* @var \yii\db\ActiveRecord[] the loaded AR models
@@ -87,23 +82,13 @@ abstract class BaseActiveFixture extends DbFixture implements \IteratorAggregate
/**
* Returns the fixture data.
*
- * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
- * The file should return the data array that will be stored in [[data]] after inserting into the database.
- *
* @return array the data to be put into the database
* @throws InvalidConfigException if the specified data file does not exist.
+ * @see [[loadDataFile]]
*/
protected function getData()
{
- if ($this->dataFile === false || $this->dataFile === null) {
- return [];
- }
- $dataFile = Yii::getAlias($this->dataFile);
- if (is_file($dataFile)) {
- return require $dataFile;
- }
-
- throw new InvalidConfigException("Fixture data file does not exist: {$this->dataFile}");
+ return $this->loadData($this->dataFile);
}
/**
diff --git a/framework/test/DbFixture.php b/framework/test/DbFixture.php
index 91744e0..423748f 100644
--- a/framework/test/DbFixture.php
+++ b/framework/test/DbFixture.php
@@ -7,7 +7,6 @@
namespace yii\test;
-use Yii;
use yii\base\BaseObject;
use yii\db\Connection;
use yii\di\Instance;
diff --git a/framework/test/FileFixtureTrait.php b/framework/test/FileFixtureTrait.php
new file mode 100644
index 0000000..6f88cf1
--- /dev/null
+++ b/framework/test/FileFixtureTrait.php
@@ -0,0 +1,59 @@
+
+ * @since 2.0.14
+ */
+trait FileFixtureTrait
+{
+ /**
+ * @var string the directory path or [path alias](guide:concept-aliases) that contains the fixture data
+ */
+ public $dataDirectory;
+ /**
+ * @var string|bool the file path or [path alias](guide:concept-aliases) of the data file that contains the fixture data
+ * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data.
+ */
+ public $dataFile;
+
+ /**
+ * Returns the fixture data.
+ *
+ * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
+ * The file should return the data array that will be stored in [[data]] after inserting into the database.
+ *
+ * @param string $file the data file path
+ * @return array the data to be put into the database
+ * @throws InvalidConfigException if the specified data file does not exist.
+ */
+ protected function loadData($file)
+ {
+ if ($file === false || $file === null) {
+ return [];
+ }
+
+ if (basename($file) === $file && $this->dataDirectory !== null) {
+ $file = $this->dataDirectory . '/' . $file;
+ }
+
+ $file = Yii::getAlias($file);
+ if (is_file($file)) {
+ return require $file;
+ }
+
+ throw new InvalidConfigException("Fixture data file does not exist: {$file}");
+ }
+
+}
diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php
index 0f50a00..92ab44a 100644
--- a/framework/validators/DateValidator.php
+++ b/framework/validators/DateValidator.php
@@ -402,7 +402,7 @@ class DateValidator extends Validator
private function parseDateValuePHP($value, $format)
{
// if no time was provided in the format string set time to 0 to get a simple date timestamp
- $hasTimeInfo = (strpbrk($format, 'HhGgis') !== false);
+ $hasTimeInfo = (strpbrk($format, 'HhGgisU') !== false);
$date = DateTime::createFromFormat($format, $value, new \DateTimeZone($hasTimeInfo ? $this->timeZone : 'UTC'));
$errors = DateTime::getLastErrors();
diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php
index c0082db..e3e9f9e 100644
--- a/framework/validators/ExistValidator.php
+++ b/framework/validators/ExistValidator.php
@@ -12,6 +12,7 @@ use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
+use yii\db\QueryInterface;
/**
* ExistValidator validates that the attribute value exists in a table.
@@ -57,6 +58,12 @@ class ExistValidator extends Validator
*/
public $targetAttribute;
/**
+ * @var string the name of the relation that should be used to validate the existence of the current attribute value
+ * This param overwrites $targetClass and $targetAttribute
+ * @since 2.0.14
+ */
+ public $targetRelation;
+ /**
* @var string|array|\Closure additional filter to be applied to the DB query used to check the existence of the attribute value.
* This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]]
* on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
@@ -72,6 +79,11 @@ class ExistValidator extends Validator
* @since 2.0.11
*/
public $targetAttributeJunction = 'and';
+ /**
+ * @var bool whether this validator is forced to always use master DB
+ * @since 2.0.14
+ */
+ public $forceMasterDb = true;
/**
@@ -90,6 +102,45 @@ class ExistValidator extends Validator
*/
public function validateAttribute($model, $attribute)
{
+ if (!empty($this->targetRelation)) {
+ $this->checkTargetRelationExistence($model, $attribute);
+ } else {
+ $this->checkTargetAttributeExistence($model, $attribute);
+ }
+ }
+
+ /**
+ * Validates existence of the current attribute based on relation name
+ * @param \yii\db\ActiveRecord $model the data model to be validated
+ * @param string $attribute the name of the attribute to be validated.
+ */
+ private function checkTargetRelationExistence($model, $attribute)
+ {
+ $exists = false;
+ /** @var ActiveQuery $relationQuery */
+ $relationQuery = $model->{'get' . ucfirst($this->targetRelation)}();
+
+ if ($this->forceMasterDb) {
+ $model::getDb()->useMaster(function() use ($relationQuery, &$exists) {
+ $exists = $relationQuery->exists();
+ });
+ } else {
+ $relationQuery->exists();
+ }
+
+
+ if (!$exists) {
+ $this->addError($model, $attribute, $this->message);
+ }
+ }
+
+ /**
+ * Validates existence of the current attribute based on targetAttribute
+ * @param \yii\base\Model $model the data model to be validated
+ * @param string $attribute the name of the attribute to be validated.
+ */
+ private function checkTargetAttributeExistence($model, $attribute)
+ {
$targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
$params = $this->prepareConditions($targetAttribute, $model, $attribute);
$conditions = [$this->targetAttributeJunction == 'or' ? 'or' : 'and'];
@@ -110,11 +161,7 @@ class ExistValidator extends Validator
$targetClass = $this->targetClass === null ? get_class($model) : $this->targetClass;
$query = $this->createQuery($targetClass, $conditions);
- if (is_array($model->$attribute)) {
- if ($query->count("DISTINCT [[$targetAttribute]]") != count($model->$attribute)) {
- $this->addError($model, $attribute, $this->message);
- }
- } elseif (!$query->exists()) {
+ if (!$this->valueExists($targetClass, $query, $model->$attribute)) {
$this->addError($model, $attribute, $this->message);
}
}
@@ -178,17 +225,53 @@ class ExistValidator extends Validator
throw new InvalidConfigException('The "targetAttribute" property must be configured as a string.');
}
+ if (is_array($value) && !$this->allowArray) {
+ return [$this->message, []];
+ }
+
$query = $this->createQuery($this->targetClass, [$this->targetAttribute => $value]);
- if (is_array($value)) {
- if (!$this->allowArray) {
- return [$this->message, []];
- }
+ return $this->valueExists($this->targetClass, $query, $value) ? null : [$this->message, []];
+ }
+
+ /**
+ * Check whether value exists in target table
+ *
+ * @param string $targetClass
+ * @param QueryInterface $query
+ * @param mixed $value the value want to be checked
+ * @return bool
+ */
+ private function valueExists($targetClass, $query, $value)
+ {
+ $db = $targetClass::getDb();
+ $exists = false;
- return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ? null : [$this->message, []];
+ if ($this->forceMasterDb) {
+ $db->useMaster(function ($db) use ($query, $value, &$exists) {
+ $exists = $this->queryValueExists($query, $value);
+ });
+ } else {
+ $exists = $this->queryValueExists($query, $value);
}
- return $query->exists() ? null : [$this->message, []];
+ return $exists;
+ }
+
+
+ /**
+ * Run query to check if value exists
+ *
+ * @param QueryInterface $query
+ * @param mixed $value the value to be checked
+ * @return bool
+ */
+ private function queryValueExists($query, $value)
+ {
+ if (is_array($value)) {
+ return $query->count("DISTINCT [[$this->targetAttribute]]") == count($value) ;
+ }
+ return $query->exists();
}
/**
diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php
index b4ebf1a..1b2b97b 100644
--- a/framework/validators/FileValidator.php
+++ b/framework/validators/FileValidator.php
@@ -77,7 +77,6 @@ class FileValidator extends Validator
* @see tooMany for the customized message when too many files are uploaded.
*/
public $maxFiles = 1;
-
/**
* @var int the minimum file count the given attribute can hold.
* Defaults to 0. Higher value means at least that number of files should be uploaded.
@@ -126,7 +125,6 @@ class FileValidator extends Validator
* - {limit}: the value of [[maxFiles]]
*/
public $tooMany;
-
/**
* @var string the error message used if the count of multiple uploads less that minFiles.
* You may use the following tokens in the message:
@@ -137,7 +135,6 @@ class FileValidator extends Validator
* @since 2.0.14
*/
public $tooFew;
-
/**
* @var string the error message used when the uploaded file has an extension name
* that is not listed in [[extensions]]. You may use the following tokens in the message:
diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php
index 194e0e4..68fa17c 100644
--- a/framework/validators/UniqueValidator.php
+++ b/framework/validators/UniqueValidator.php
@@ -93,6 +93,11 @@ class UniqueValidator extends Validator
* @since 2.0.11
*/
public $targetAttributeJunction = 'and';
+ /**
+ * @var bool whether this validator is forced to always use master DB
+ * @since 2.0.14
+ */
+ public $forceMasterDb = true;
/**
@@ -135,7 +140,19 @@ class UniqueValidator extends Validator
$conditions[] = [$key => $value];
}
- if ($this->modelExists($targetClass, $conditions, $model)) {
+ $db = $targetClass::getDb();
+
+ $modelExists = false;
+
+ if ($this->forceMasterDb && method_exists($db, 'useMaster')) {
+ $db->useMaster(function () use ($targetClass, $conditions, $model, &$modelExists) {
+ $modelExists = $this->modelExists($targetClass, $conditions, $model);
+ });
+ } else {
+ $modelExists = $this->modelExists($targetClass, $conditions, $model);
+ }
+
+ if ($modelExists) {
if (is_array($targetAttribute) && count($targetAttribute) > 1) {
$this->addComboNotUniqueError($model, $attribute);
} else {
diff --git a/framework/views/errorHandler/callStackItem.php b/framework/views/errorHandler/callStackItem.php
index 04f8c88..512181d 100644
--- a/framework/views/errorHandler/callStackItem.php
+++ b/framework/views/errorHandler/callStackItem.php
@@ -34,12 +34,14 @@