diff --git a/.gitignore b/.gitignore
index 6482763..5586ab2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,13 +13,15 @@ nbproject
Thumbs.db
# composer vendor dir
-/yii/vendor
+/vendor
# composer itself is not needed
composer.phar
+# composer.lock should not be committed as we always want the latest versions
+/composer.lock
# Mac DS_Store Files
.DS_Store
# local phpunit config
-/phpunit.xml
\ No newline at end of file
+/phpunit.xml
diff --git a/.travis.yml b/.travis.yml
index 7e5a002..6eeec4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,18 +7,22 @@ php:
services:
- redis-server
- memcached
+ - elasticsearch
before_script:
- composer self-update && composer --version
- composer require satooshi/php-coveralls 0.6.* --dev --prefer-dist
- mysql -e 'CREATE DATABASE yiitest;';
+ - mysql -D yiitest -u travis < /home/travis/build/yiisoft/yii2/tests/unit/data/sphinx/source.sql
- psql -U postgres -c 'CREATE DATABASE yiitest;';
+ - echo 'elasticsearch version ' && curl http://localhost:9200/
- tests/unit/data/travis/apc-setup.sh
- tests/unit/data/travis/memcache-setup.sh
- tests/unit/data/travis/cubrid-setup.sh
+ - tests/unit/data/travis/sphinx-setup.sh
-script:
- - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor,sphinx
+#script:
+# - phpunit --coverage-clover tests/unit/runtime/coveralls/clover.xml --verbose --exclude-group mssql,oci,wincache,xcache,zenddata,vendor,sphinx
-after_script:
- - php vendor/bin/coveralls
+#after_script:
+# - php vendor/bin/coveralls
diff --git a/README.md b/README.md
index 3bc2d67..35a50f5 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ without prior notices. **Yii 2.0 is not ready for production use yet.**
[![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)
-[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
+[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2)
[![Dependency Status](https://www.versioneye.com/php/yiisoft:yii2/dev-master/badge.png)](https://www.versioneye.com/php/yiisoft:yii2/dev-master)
@@ -24,7 +24,7 @@ DIRECTORY STRUCTURE
benchmark/ app demonstrating the minimal overhead introduced by the framework
build/ internally used build tools
docs/ documentation
- extensions/ extensions
+ extensions/ extensions
framework/ framework files
yii/ framework source files
tests/ tests of the core framework code
diff --git a/apps/advanced/backend/views/layouts/main.php b/apps/advanced/backend/views/layouts/main.php
index fdffc26..1857e62 100644
--- a/apps/advanced/backend/views/layouts/main.php
+++ b/apps/advanced/backend/views/layouts/main.php
@@ -11,16 +11,16 @@ use yii\widgets\Breadcrumbs;
*/
AppAsset::register($this);
?>
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
- beginBody(); ?>
+ beginBody() ?>
'My Company',
@@ -58,7 +58,7 @@ AppAsset::register($this);
- endBody(); ?>
+ endBody() ?>
-endPage(); ?>
+endPage() ?>
diff --git a/apps/advanced/common/config/params.php b/apps/advanced/common/config/params.php
index 4d83098..c378364 100644
--- a/apps/advanced/common/config/params.php
+++ b/apps/advanced/common/config/params.php
@@ -3,6 +3,7 @@
Yii::setAlias('common', realpath(__DIR__ . '/../'));
Yii::setAlias('frontend', realpath(__DIR__ . '/../../frontend'));
Yii::setAlias('backend', realpath(__DIR__ . '/../../backend'));
+Yii::setAlias('console', realpath(__DIR__ . '/../../console'));
return [
'adminEmail' => 'admin@example.com',
@@ -14,6 +15,7 @@ return [
'components.mail' => [
'class' => 'yii\swiftmailer\Mailer',
+ 'viewPath' => '@common/mails',
],
'components.db' => [
diff --git a/apps/advanced/common/mails/layouts/html.php b/apps/advanced/common/mails/layouts/html.php
new file mode 100644
index 0000000..2e6b615
--- /dev/null
+++ b/apps/advanced/common/mails/layouts/html.php
@@ -0,0 +1,24 @@
+
+beginPage() ?>
+
+
+
+
+ = Html::encode($this->title) ?>
+ head() ?>
+
+
+ beginBody() ?>
+ = $content ?>
+ endBody() ?>
+
+
+endPage() ?>
\ No newline at end of file
diff --git a/apps/advanced/frontend/views/emails/passwordResetToken.php b/apps/advanced/common/mails/passwordResetToken.php
similarity index 100%
rename from apps/advanced/frontend/views/emails/passwordResetToken.php
rename to apps/advanced/common/mails/passwordResetToken.php
diff --git a/apps/advanced/console/migrations/m130524_201442_init.php b/apps/advanced/console/migrations/m130524_201442_init.php
index 1315d8d..a5935be 100644
--- a/apps/advanced/console/migrations/m130524_201442_init.php
+++ b/apps/advanced/console/migrations/m130524_201442_init.php
@@ -6,19 +6,21 @@ class m130524_201442_init extends \yii\db\Migration
{
public function up()
{
- // MySQL-specific table options. Adjust if you plan working with another DBMS
- $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
+ $tableOptions = null;
+ if ($this->db->driverName === 'mysql') {
+ $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
+ }
$this->createTable('tbl_user', [
'id' => Schema::TYPE_PK,
- 'username' => Schema::TYPE_STRING.' NOT NULL',
- 'auth_key' => Schema::TYPE_STRING.'(32) NOT NULL',
- 'password_hash' => Schema::TYPE_STRING.' NOT NULL',
- 'password_reset_token' => Schema::TYPE_STRING.'(32)',
- 'email' => Schema::TYPE_STRING.' NOT NULL',
- 'role' => 'tinyint NOT NULL DEFAULT 10',
+ 'username' => Schema::TYPE_STRING . ' NOT NULL',
+ 'auth_key' => Schema::TYPE_STRING . '(32) NOT NULL',
+ 'password_hash' => Schema::TYPE_STRING . ' NOT NULL',
+ 'password_reset_token' => Schema::TYPE_STRING . '(32)',
+ 'email' => Schema::TYPE_STRING . ' NOT NULL',
+ 'role' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10',
- 'status' => 'tinyint NOT NULL DEFAULT 10',
+ 'status' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 10',
'create_time' => Schema::TYPE_INTEGER.' NOT NULL',
'update_time' => Schema::TYPE_INTEGER.' NOT NULL',
], $tableOptions);
diff --git a/apps/advanced/frontend/controllers/SiteController.php b/apps/advanced/frontend/controllers/SiteController.php
index 184d16c..edb9cd9 100644
--- a/apps/advanced/frontend/controllers/SiteController.php
+++ b/apps/advanced/frontend/controllers/SiteController.php
@@ -159,17 +159,11 @@ class SiteController extends Controller
$user->password_reset_token = Security::generateRandomKey();
if ($user->save(false)) {
- // todo: refactor it with mail component. pay attention to the arrangement of mail view files
- $fromEmail = \Yii::$app->params['supportEmail'];
- $name = '=?UTF-8?B?' . base64_encode(\Yii::$app->name . ' robot') . '?=';
- $subject = '=?UTF-8?B?' . base64_encode('Password reset for ' . \Yii::$app->name) . '?=';
- $body = $this->renderPartial('/emails/passwordResetToken', [
- 'user' => $user,
- ]);
- $headers = "From: $name <{$fromEmail}>\r\n" .
- "MIME-Version: 1.0\r\n" .
- "Content-type: text/plain; charset=UTF-8";
- return mail($email, $subject, $body, $headers);
+ return \Yii::$app->mail->compose('passwordResetToken', ['user' => $user])
+ ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name . ' robot'])
+ ->setTo($email)
+ ->setSubject('Password reset for ' . \Yii::$app->name)
+ ->send();
}
return false;
diff --git a/apps/advanced/frontend/views/layouts/main.php b/apps/advanced/frontend/views/layouts/main.php
index febcc5a..7bc03d2 100644
--- a/apps/advanced/frontend/views/layouts/main.php
+++ b/apps/advanced/frontend/views/layouts/main.php
@@ -12,16 +12,16 @@ use frontend\widgets\Alert;
*/
AppAsset::register($this);
?>
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
- beginBody(); ?>
+ beginBody() ?>
'My Company',
@@ -63,7 +63,7 @@ AppAsset::register($this);
- endBody(); ?>
+ endBody() ?>
-endPage(); ?>
+endPage() ?>
diff --git a/apps/advanced/frontend/widgets/Alert.php b/apps/advanced/frontend/widgets/Alert.php
index e070e1b..390f359 100644
--- a/apps/advanced/frontend/widgets/Alert.php
+++ b/apps/advanced/frontend/widgets/Alert.php
@@ -27,13 +27,13 @@ class Alert extends \yii\bootstrap\Widget
* - $value is the bootstrap alert type (i.e. danger, success, info, warning)
*/
public $alertTypes = [
- 'error' => 'danger',
- 'danger' => 'danger',
- 'success' => 'success',
- 'info' => 'info',
+ 'error' => 'danger',
+ 'danger' => 'danger',
+ 'success' => 'success',
+ 'info' => 'info',
'warning' => 'warning'
];
-
+
/**
* @var array the options for rendering the close button tag.
*/
@@ -49,7 +49,7 @@ class Alert extends \yii\bootstrap\Widget
foreach ($flashes as $type => $message) {
/* initialize css class for each alert box */
- $this->options['class'] = 'alert-' . $this->alertTypes[$type] . $appendCss;
+ $this->options['class'] = 'alert-' . $this->alertTypes[$type] . $appendCss;
/* assign unique id to each alert box */
$this->options['id'] = $this->getId() . '-' . $type;
diff --git a/apps/basic/commands/HelloController.php b/apps/basic/commands/HelloController.php
index 29394cd..db36c5a 100644
--- a/apps/basic/commands/HelloController.php
+++ b/apps/basic/commands/HelloController.php
@@ -13,7 +13,7 @@ use yii\console\Controller;
* This command echoes what the first argument that you have entered.
*
* This command is provided as an example for you to learn how to create console commands.
- *
+ *
* @author Qiang Xue
* @since 2.0
*/
diff --git a/apps/basic/tests/_helpers/CodeHelper.php b/apps/basic/tests/_helpers/CodeHelper.php
index 972c8f3..eea532c 100644
--- a/apps/basic/tests/_helpers/CodeHelper.php
+++ b/apps/basic/tests/_helpers/CodeHelper.php
@@ -1,7 +1,7 @@
dontSeeElement('.error');
@@ -834,7 +834,7 @@ class WebGuy extends \Codeception\AbstractGuy
* Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath
*
* Example:
- *
+ *
* ``` php
* dontSeeElement('.error');
@@ -854,7 +854,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -873,7 +873,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -892,7 +892,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -911,16 +911,16 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
* ----------------------------------------------
*
* Fills a text field or textarea with value.
- *
+ *
* Example:
- *
+ *
* ``` php
* fillField("//input[@type='text']", "Hello World!");
@@ -941,7 +941,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -981,7 +981,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1011,7 +1011,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1040,7 +1040,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1099,7 +1099,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1152,7 +1152,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1209,7 +1209,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1266,7 +1266,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1321,7 +1321,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1376,7 +1376,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1419,7 +1419,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1462,7 +1462,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1485,7 +1485,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1507,7 +1507,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1529,7 +1529,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1560,7 +1560,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1591,7 +1591,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1648,7 +1648,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1705,7 +1705,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1774,7 +1774,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1841,7 +1841,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1874,7 +1874,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1907,7 +1907,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1962,7 +1962,7 @@ class WebGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
diff --git a/apps/basic/tests/functional/TestGuy.php b/apps/basic/tests/functional/TestGuy.php
index 553a44f..9da8947 100644
--- a/apps/basic/tests/functional/TestGuy.php
+++ b/apps/basic/tests/functional/TestGuy.php
@@ -27,7 +27,7 @@ use Codeception\Module\Yii2;
class TestGuy extends \Codeception\AbstractGuy
{
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -49,7 +49,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -79,7 +79,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -106,7 +106,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -133,7 +133,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -161,7 +161,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -220,7 +220,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -281,7 +281,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -336,7 +336,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -355,7 +355,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -412,7 +412,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -439,13 +439,13 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
* ----------------------------------------------
*
- * Authenticates user for HTTP_AUTH
+ * Authenticates user for HTTP_AUTH
*
* @param $username
* @param $password
@@ -461,7 +461,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -494,7 +494,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -540,7 +540,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -605,7 +605,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -670,7 +670,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -733,7 +733,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -794,7 +794,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -853,7 +853,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -906,7 +906,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -963,7 +963,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1020,7 +1020,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1075,7 +1075,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1130,7 +1130,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1161,7 +1161,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1224,7 +1224,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1285,7 +1285,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1354,7 +1354,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1421,7 +1421,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1477,16 +1477,16 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
* ----------------------------------------------
*
* Fills a text field or textarea with value.
- *
+ *
* Example:
- *
+ *
* ``` php
* fillField("//input[@type='text']", "Hello World!");
@@ -1507,7 +1507,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1547,7 +1547,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1577,7 +1577,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1606,7 +1606,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1637,7 +1637,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1662,7 +1662,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1698,7 +1698,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1731,7 +1731,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1764,7 +1764,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1817,7 +1817,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1826,7 +1826,7 @@ class TestGuy extends \Codeception\AbstractGuy
* Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath
*
* Example:
- *
+ *
* ``` php
* dontSeeElement('.error');
@@ -1854,7 +1854,7 @@ class TestGuy extends \Codeception\AbstractGuy
* Checks if element does not exist (or is visible) on a page, matching it by CSS or XPath
*
* Example:
- *
+ *
* ``` php
* dontSeeElement('.error');
@@ -1874,7 +1874,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1931,7 +1931,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -1988,7 +1988,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -2025,7 +2025,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -2068,7 +2068,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
@@ -2123,7 +2123,7 @@ class TestGuy extends \Codeception\AbstractGuy
return new Maybe();
}
-
+
/**
* This method is generated.
* Documentation taken from corresponding module.
diff --git a/apps/basic/tests/unit/CodeGuy.php b/apps/basic/tests/unit/CodeGuy.php
index 613c754..70165a6 100644
--- a/apps/basic/tests/unit/CodeGuy.php
+++ b/apps/basic/tests/unit/CodeGuy.php
@@ -25,6 +25,6 @@ use Codeception\Module\CodeHelper;
class CodeGuy extends \Codeception\AbstractGuy
{
-
+
}
diff --git a/apps/basic/views/layouts/main.php b/apps/basic/views/layouts/main.php
index 04acdfb..e99b3c5 100644
--- a/apps/basic/views/layouts/main.php
+++ b/apps/basic/views/layouts/main.php
@@ -11,16 +11,16 @@ use app\assets\AppAsset;
*/
AppAsset::register($this);
?>
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
-beginBody(); ?>
+beginBody() ?>
'My Company',
@@ -59,7 +59,7 @@ AppAsset::register($this);
-endBody(); ?>
+endBody() ?>
-endPage(); ?>
+endPage() ?>
diff --git a/build/build b/build/build
index b1d6bb2..daf5e09 100755
--- a/build/build
+++ b/build/build
@@ -11,6 +11,8 @@
// fcgi doesn't have STDIN defined by default
defined('STDIN') or define('STDIN', fopen('php://stdin', 'r'));
+define('YII_DEBUG', true);
+
require(__DIR__ . '/../framework/yii/Yii.php');
$application = new yii\console\Application([
diff --git a/build/controllers/PhpDocController.php b/build/controllers/PhpDocController.php
index c7a2fa7..0dc0f24 100644
--- a/build/controllers/PhpDocController.php
+++ b/build/controllers/PhpDocController.php
@@ -7,6 +7,7 @@
namespace yii\build\controllers;
+use Yii;
use yii\console\Controller;
use yii\helpers\Console;
use yii\helpers\FileHelper;
@@ -38,10 +39,30 @@ class PhpDocController extends Controller
*
* @param null $root the directory to parse files from. Defaults to YII_PATH.
*/
- public function actionProperty($root=null)
+ public function actionProperty($root = null)
{
+ $except = [];
if ($root === null) {
- $root = YII_PATH;
+ $root = dirname(dirname(YII_PATH));
+ Yii::setAlias('@yii/bootstrap', $root . '/extensions/bootstrap');
+ Yii::setAlias('@yii/debug', $root . '/extensions/debug');
+ Yii::setAlias('@yii/elasticsearch', $root . '/extensions/elasticsearch');
+ Yii::setAlias('@yii/gii', $root . '/extensions/gii');
+ Yii::setAlias('@yii/jui', $root . '/extensions/jui');
+ Yii::setAlias('@yii/redis', $root . '/extensions/redis');
+ Yii::setAlias('@yii/smarty', $root . '/extensions/smarty');
+ Yii::setAlias('@yii/sphinx', $root . '/extensions/sphinx');
+ Yii::setAlias('@yii/swiftmailer', $root . '/extensions/swiftmailer');
+ Yii::setAlias('@yii/twig', $root . '/extensions/twig');
+
+ $except = [
+ '/apps/',
+ '/build/',
+ '/docs/',
+ '/extensions/composer/',
+ '/tests/',
+ '/vendor/',
+ ];
}
$root = FileHelper::normalizePath($root);
$options = [
@@ -55,14 +76,13 @@ class PhpDocController extends Controller
return null;
},
'only' => ['.php'],
- 'except' => [
+ 'except' => array_merge($except, [
'BaseYii.php',
'Yii.php',
- '/debug/views/',
+ '/views/',
'/requirements/',
- '/gii/views/',
'/gii/generators/',
- ],
+ ]),
];
$files = FileHelper::findFiles($root, $options);
$nFilesTotal = 0;
@@ -216,20 +236,27 @@ class PhpDocController extends Controller
$ns = $this->match('#\nnamespace (?[\w\\\\]+);\n#', $file);
$namespace = reset($ns);
$namespace = $namespace['name'];
- $classes = $this->match('#\n(?:abstract )?class (?\w+)( |\n)(extends )?.+\{(?.*)\n\}(\n|$)#', $file);
+ $classes = $this->match('#\n(?:abstract )?class (?\w+)( extends .+)?( implements .+)?\n\{(?.*)\n\}(\n|$)#', $file);
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) {
- $interfaces = $this->match('#\ninterface (?\w+)\n\{(?.+)\n\}(\n|$)#', $file);
+ $interfaces = $this->match('#\ninterface (?\w+)( extends .+)?\n\{(?.+)\n\}(\n|$)#', $file);
if (count($interfaces) == 1) {
return false;
} elseif (count($interfaces) > 1) {
$this->stderr("[ERR] There should be only one interface in a file: $fileName\n", Console::FG_RED);
} else {
- $this->stderr("[ERR] No class in file: $fileName\n", Console::FG_RED);
+ $traits = $this->match('#\ntrait (?\w+)\n\{(?.+)\n\}(\n|$)#', $file);
+ if (count($traits) == 1) {
+ return false;
+ } 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);
+ }
}
return false;
}
diff --git a/docs/guide/active-record.md b/docs/guide/active-record.md
index 90826b0..aa47751 100644
--- a/docs/guide/active-record.md
+++ b/docs/guide/active-record.md
@@ -3,7 +3,7 @@ Active Record
Active Record implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record).
The premise behind Active Record is that an individual [[ActiveRecord]] object is associated with a specific row in a database table. The object's attributes are mapped to the columns of the corresponding table. Referencing an Active Record attribute is equivalent to accessing
-the corresponding table column for that record.
+the corresponding table column for that record.
As an example, say that the `Customer` ActiveRecord class is associated with the
`tbl_customer` table. This would mean that the class's `name` attribute is automatically mapped to the `name` column in `tbl_customer`.
diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md
index 0b81f14..e73b11d 100644
--- a/docs/guide/authentication.md
+++ b/docs/guide/authentication.md
@@ -3,7 +3,7 @@ Authentication
Authentication is the act of verifying who a user is, and is the basis of the login process. Typically, authentication uses an identifier--a username or email address--and password, submitted through a form. The application then compares this information against that previously stored.
-In Yii all this is done semi-automatically, leaving the developer to merely implement [[\yii\web\IdentityInterface]]. Typically, implementation is accomplished using the `User` model. You can find a full featured example in the
+In Yii all this is done semi-automatically, leaving the developer to merely implement [[\yii\web\IdentityInterface]]. Typically, implementation is accomplished using the `User` model. You can find a full featured example in the
[advanced application template](installation.md). Below only the interface methods are listed:
```php
diff --git a/docs/guide/composer.md b/docs/guide/composer.md
index 89eac35..8abef5a 100644
--- a/docs/guide/composer.md
+++ b/docs/guide/composer.md
@@ -9,7 +9,7 @@ Installing Composer
In order to install Composer, check the official guide for your operating system:
-* [Linux](http://getcomposer.org/doc/00-intro.md#installation-nix)
+* [Linux](http://getcomposer.org/doc/00-intro.md#installation-nix)
* [Windows](http://getcomposer.org/doc/00-intro.md#installation-windows)
All of the details can be found in the guide, but you'll either download Composer directly from [http://getcomposer.org/](http://getcomposer.org/), or run the following command:
diff --git a/docs/guide/controller.md b/docs/guide/controller.md
index a668ba7..571a833 100644
--- a/docs/guide/controller.md
+++ b/docs/guide/controller.md
@@ -167,7 +167,7 @@ public SiteController extends \yii\web\Controller
{
return [
'about' => [
- 'class' => '@app/actions/Page',
+ 'class' => 'app\actions\Page',
'view' => 'about',
],
];
diff --git a/docs/guide/form.md b/docs/guide/form.md
index d31d653..5f7082a 100644
--- a/docs/guide/form.md
+++ b/docs/guide/form.md
@@ -88,3 +88,19 @@ customize the output, you can chain additional methods to this call:
= $form->field($model, 'username')->textInput()->hint('Please enter your name')->label('Name') ?>
```
+
+This will create all the ``, ` ` and other tags according to the template defined by the form field.
+To add these tags yourself you can use the `Html` helper class. The following is equivalent to the code above:
+
+```php
+= Html::activeLabel($model, 'password') ?>
+= Html::activePasswordInput($model, 'password') ?>
+= Html::error($model, 'password') ?>
+
+or
+
+= Html::activeLabel($model, 'username', ['label' => 'name']) ?>
+= Html::activeTextInput($model, 'username') ?>
+= Html::error($model, 'username') ?>
+Please enter your name
+```
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 1575163..6a99458 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -19,7 +19,7 @@ curl -s http://getcomposer.org/installer | php
For problems or more information, see the official Composer guide:
-* [Linux](http://getcomposer.org/doc/00-intro.md#installation-nix)
+* [Linux](http://getcomposer.org/doc/00-intro.md#installation-nix)
* [Windows](http://getcomposer.org/doc/00-intro.md#installation-windows)
With Composer installed, you can create a new Yii site using one of Yii's ready-to-use application templates.
diff --git a/docs/guide/model.md b/docs/guide/model.md
index abf2b48..a02e1ad 100644
--- a/docs/guide/model.md
+++ b/docs/guide/model.md
@@ -59,7 +59,7 @@ Attribute Labels
----------------
Attribute labels are mainly used for display purpose. For example, given an attribute `firstName`, we can declare
-a label `First Name` that is more user-friendly when displayed to end users in places such as form labels and
+a label `First Name` that is more user-friendly when displayed to end users in places such as form labels and
error messages. Given an attribute name, you can obtain its label by calling [[\yii\base\Model::getAttributeLabel()]].
To declare attribute labels, override the [[\yii\base\Model::attributeLabels()]] method. The overridden method returns a mapping of attribute names to attribute labels, as shown in the example below. If an attribute is not found
@@ -86,7 +86,7 @@ Scenarios
---------
A model may be used in different *scenarios*. For example, a `User` model may be used to collect user login inputs,
-but it may also be used for user registration purposes. In the one scenario, every piece of data is required; in the other, only the username and password would be.
+but it may also be used for user registration purposes. In the one scenario, every piece of data is required; in the other, only the username and password would be.
To easily implement the business logic for different scenarios, each model has a property named `scenario`
that stores the name of the scenario that the model is currently being used in. As will be explained in the next
diff --git a/docs/guide/mvc.md b/docs/guide/mvc.md
index 22f86a1..8579064 100644
--- a/docs/guide/mvc.md
+++ b/docs/guide/mvc.md
@@ -3,7 +3,7 @@ MVC Overview
Yii implements the model-view-controller (MVC) design pattern, which is
widely adopted in web and other application programming. MVC aims to separate business logic from
-user interface considerations, allowing developers to more easily change individual components of an application without affecting, or even touching, another.
+user interface considerations, allowing developers to more easily change individual components of an application without affecting, or even touching, another.
In MVC, the *model* represents the
information (the data) and the business rules to which the data must adhere. The *view* contains elements
diff --git a/docs/guide/security.md b/docs/guide/security.md
index f54a133..d74a661 100644
--- a/docs/guide/security.md
+++ b/docs/guide/security.md
@@ -1,7 +1,7 @@
Security
========
-Good security is vital to the health and success of many websites. Unfortunately, many developers may cut corners when it comes to security due to a lack of understanding or too large of an implementation hurdle. To make your Yii-based site as secure as possible, the Yii framework has baked in several excellent, and easy to use, security features.
+Good security is vital to the health and success of many websites. Unfortunately, many developers may cut corners when it comes to security due to a lack of understanding or too large of an implementation hurdle. To make your Yii-based site as secure as possible, the Yii framework has baked in several excellent, and easy to use, security features.
Hashing and verifying passwords
-------------------------------
diff --git a/docs/guide/template.md b/docs/guide/template.md
index 26c9707..b4afd33 100644
--- a/docs/guide/template.md
+++ b/docs/guide/template.md
@@ -1,10 +1,11 @@
Using template engines
======================
-By default Yii uses PHP as template language, but you can configure it to support other rendering engines, such as [Twig](http://twig.sensiolabs.org/) or [Smarty](http://www.smarty.net/).
+By default Yii uses PHP as template language, but you can configure it to support other rendering engines, such as
+[Twig](http://twig.sensiolabs.org/) or [Smarty](http://www.smarty.net/).
-The `view` component is responsible for rendering views. You can add
-a custom template engines by reconfiguring this component's behavior:
+The `view` component is responsible for rendering views. You can add a custom template engines by reconfiguring this
+component's behavior:
```php
[
@@ -13,11 +14,13 @@ a custom template engines by reconfiguring this component's behavior:
'class' => 'yii\web\View',
'renderers' => [
'tpl' => [
- 'class' => 'yii\renderers\SmartyViewRenderer',
+ 'class' => 'yii\smarty\ViewRenderer',
+ //'cachePath' => '@runtime/Smarty/cache',
],
'twig' => [
- 'class' => 'yii\renderers\TwigViewRenderer',
- 'twigPath' => '@app/vendors/Twig',
+ 'class' => 'yii\twig\ViewRenderer',
+ //'cachePath' => '@runtime/Twig/cache',
+ //'options' => [], /* Array of twig options */
],
// ...
],
@@ -26,7 +29,15 @@ a custom template engines by reconfiguring this component's behavior:
]
```
-Note that the Smarty and Twig packages themselves are not bundled with Yii. You must download them yourself. Then unpack the packages and place the resulting files in a logical location, such as the application's `protected/vendor` folder. Finally, specify the correct `smartyPath` or `twigPath`, as in the code above (for Twig).
+In the config above we're using Smarty and Twig. In order to get these extensions in your project you need to modify
+your `composer.json` to include
+
+```
+"yiisoft/yii2-smarty": "*",
+"yiisoft/yii2-twig": "*",
+```
+
+in `require` section and then run `composer update`.
Twig
----
diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md
index 37a7511..c02a48d 100644
--- a/docs/guide/upgrade-from-v1.md
+++ b/docs/guide/upgrade-from-v1.md
@@ -135,7 +135,7 @@ Path alias is also closely related with class namespaces. It is recommended that
alias be defined for each root namespace so that you can use Yii the class autoloader without
any further configuration. For example, because `@yii` refers to the Yii installation directory,
a class like `yii\web\Request` can be autoloaded by Yii. If you use a third party library
-such as Zend Framework, you may define a path alias `@Zend` which refers to its installation
+such as Zend Framework, you may define a path alias `@Zend` which refers to its installation
directory and Yii will be able to autoload any class in this library.
More on path aliases can be found in the [Basic concepts section](basics.md).
diff --git a/docs/guide/view.md b/docs/guide/view.md
index ae86809..7161a5a 100644
--- a/docs/guide/view.md
+++ b/docs/guide/view.md
@@ -254,24 +254,24 @@ basic one without any widgets or extra markup.
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
-beginBody(); ?>
+beginBody() ?>
= $content ?>
-endBody(); ?>
+endBody() ?>
-endPage(); ?>
+endPage() ?>
```
In the markup above there's some code. First of all, `$content` is a variable that will contain result of views rendered
diff --git a/extensions/bootstrap/Nav.php b/extensions/bootstrap/Nav.php
index 8f0a98d..ef45f09 100644
--- a/extensions/bootstrap/Nav.php
+++ b/extensions/bootstrap/Nav.php
@@ -14,7 +14,7 @@ use yii\helpers\Html;
/**
* Nav renders a nav HTML component.
- *
+ *
* For example:
*
* ```php
@@ -37,12 +37,12 @@ use yii\helpers\Html;
* ],
* ]);
* ```
- *
+ *
* Note: Multilevel dropdowns beyond Level 1 are not supported in Bootstrap 3.
- *
+ *
* @see http://getbootstrap.com/components.html#dropdowns
* @see http://getbootstrap.com/components/#nav
- *
+ *
* @author Antonio Ramirez
* @since 2.0
*/
diff --git a/extensions/bootstrap/composer.json b/extensions/bootstrap/composer.json
index e80de80..3e6031e 100644
--- a/extensions/bootstrap/composer.json
+++ b/extensions/bootstrap/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Abootstrap",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/composer/Installer.php b/extensions/composer/Installer.php
index d8d799f..164392e 100644
--- a/extensions/composer/Installer.php
+++ b/extensions/composer/Installer.php
@@ -26,7 +26,7 @@ class Installer extends LibraryInstaller
const EXTENSION_FILE = 'yiisoft/extensions.php';
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function supports($packageType)
{
@@ -34,7 +34,7 @@ class Installer extends LibraryInstaller
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
{
@@ -49,7 +49,7 @@ class Installer extends LibraryInstaller
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
{
@@ -63,7 +63,7 @@ class Installer extends LibraryInstaller
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
{
diff --git a/extensions/composer/Plugin.php b/extensions/composer/Plugin.php
index 1111738..40bd8e5 100644
--- a/extensions/composer/Plugin.php
+++ b/extensions/composer/Plugin.php
@@ -20,7 +20,7 @@ use Composer\Plugin\PluginInterface;
class Plugin implements PluginInterface
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function activate(Composer $composer, IOInterface $io)
{
diff --git a/extensions/debug/composer.json b/extensions/debug/composer.json
index f60d1df..d8cbc1e 100644
--- a/extensions/debug/composer.json
+++ b/extensions/debug/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Adebug",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/debug/views/layouts/main.php b/extensions/debug/views/layouts/main.php
index 5760c3a..97ef08c 100644
--- a/extensions/debug/views/layouts/main.php
+++ b/extensions/debug/views/layouts/main.php
@@ -9,15 +9,15 @@ yii\debug\DebugAsset::register($this);
?>
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
-beginBody(); ?>
+beginBody() ?>
= $content ?>
-endBody(); ?>
+endBody() ?>
-endPage(); ?>
+endPage() ?>
diff --git a/extensions/elasticsearch/ActiveQuery.php b/extensions/elasticsearch/ActiveQuery.php
new file mode 100644
index 0000000..b444f05
--- /dev/null
+++ b/extensions/elasticsearch/ActiveQuery.php
@@ -0,0 +1,199 @@
+with('orders')->asArray()->all();
+ * ~~~
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class ActiveQuery extends Query implements ActiveQueryInterface
+{
+ use ActiveQueryTrait;
+
+ /**
+ * Creates a DB command that can be used to execute this query.
+ * @param Connection $db the DB connection used to create the DB command.
+ * If null, the DB connection returned by [[modelClass]] will be used.
+ * @return Command the created DB command instance.
+ */
+ public function createCommand($db = null)
+ {
+ /** @var ActiveRecord $modelClass */
+ $modelClass = $this->modelClass;
+ if ($db === null) {
+ $db = $modelClass::getDb();
+ }
+
+ if ($this->type === null) {
+ $this->type = $modelClass::type();
+ }
+ if ($this->index === null) {
+ $this->index = $modelClass::index();
+ $this->type = $modelClass::type();
+ }
+ $commandConfig = $db->getQueryBuilder()->build($this);
+ return $db->createCommand($commandConfig);
+ }
+
+ /**
+ * Executes query and returns all results as an array.
+ * @param Connection $db the DB connection used to create the DB command.
+ * If null, the DB connection returned by [[modelClass]] will be used.
+ * @return array the query results. If the query results in nothing, an empty array will be returned.
+ */
+ public function all($db = null)
+ {
+ $result = $this->createCommand($db)->search();
+ if (empty($result['hits']['hits'])) {
+ return [];
+ }
+ if ($this->fields !== null) {
+ foreach ($result['hits']['hits'] as &$row) {
+ $row['_source'] = isset($row['fields']) ? $row['fields'] : [];
+ unset($row['fields']);
+ }
+ unset($row);
+ }
+ if ($this->asArray && $this->indexBy) {
+ foreach ($result['hits']['hits'] as &$row) {
+ $row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id'];
+ $row = $row['_source'];
+ }
+ }
+ $models = $this->createModels($result['hits']['hits']);
+ if ($this->asArray && !$this->indexBy) {
+ foreach($models as $key => $model) {
+ $model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id'];
+ $models[$key] = $model['_source'];
+ }
+ }
+ if (!empty($this->with)) {
+ $this->findWith($this->with, $models);
+ }
+ return $models;
+ }
+
+ /**
+ * Executes query and returns a single row of result.
+ * @param Connection $db the DB connection used to create the DB command.
+ * If null, the DB connection returned by [[modelClass]] will be used.
+ * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
+ * the query result may be either an array or an ActiveRecord object. Null will be returned
+ * if the query results in nothing.
+ */
+ public function one($db = null)
+ {
+ if (($result = parent::one($db)) === false) {
+ return null;
+ }
+ if ($this->asArray) {
+ $model = $result['_source'];
+ $model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id'];
+ } else {
+ /** @var ActiveRecord $class */
+ $class = $this->modelClass;
+ $model = $class::create($result);
+ }
+ if (!empty($this->with)) {
+ $models = [$model];
+ $this->findWith($this->with, $models);
+ $model = $models[0];
+ }
+ return $model;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search($db = null, $options = [])
+ {
+ $result = $this->createCommand($db)->search($options);
+ if (!empty($result['hits']['hits'])) {
+ $models = $this->createModels($result['hits']['hits']);
+ if ($this->asArray) {
+ foreach($models as $key => $model) {
+ $model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id'];
+ $models[$key] = $model['_source'];
+ }
+ }
+ if (!empty($this->with)) {
+ $this->findWith($this->with, $models);
+ }
+ $result['hits']['hits'] = $models;
+ }
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function scalar($field, $db = null)
+ {
+ $record = parent::one($db);
+ if ($record !== false) {
+ if ($field == ActiveRecord::PRIMARY_KEY_NAME) {
+ return $record['_id'];
+ } elseif (isset($record['_source'][$field])) {
+ return $record['_source'][$field];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function column($field, $db = null)
+ {
+ if ($field == ActiveRecord::PRIMARY_KEY_NAME) {
+ $command = $this->createCommand($db);
+ $command->queryParts['fields'] = [];
+ $result = $command->search();
+ if (empty($result['hits']['hits'])) {
+ return [];
+ }
+ $column = [];
+ foreach ($result['hits']['hits'] as $row) {
+ $column[] = $row['_id'];
+ }
+ return $column;
+ }
+ return parent::column($field, $db);
+ }
+}
diff --git a/extensions/elasticsearch/ActiveRecord.php b/extensions/elasticsearch/ActiveRecord.php
new file mode 100644
index 0000000..efdb5fe
--- /dev/null
+++ b/extensions/elasticsearch/ActiveRecord.php
@@ -0,0 +1,474 @@
+
+ * @since 2.0
+ */
+class ActiveRecord extends \yii\db\ActiveRecord
+{
+ const PRIMARY_KEY_NAME = 'id';
+
+ private $_id;
+ private $_version;
+
+ /**
+ * Returns the database connection used by this AR class.
+ * By default, the "elasticsearch" application component is used as the database connection.
+ * You may override this method if you want to use a different database connection.
+ * @return Connection the database connection used by this AR class.
+ */
+ public static function getDb()
+ {
+ return \Yii::$app->getComponent('elasticsearch');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function find($q = null)
+ {
+ $query = static::createQuery();
+ if (is_array($q)) {
+ if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q))) {
+ $pk = $q[ActiveRecord::PRIMARY_KEY_NAME];
+ if (is_array($pk)) {
+ return static::mget($pk);
+ } else {
+ return static::get($pk);
+ }
+ }
+ return $query->where($q)->one();
+ } elseif ($q !== null) {
+ return static::get($q);
+ }
+ return $query;
+ }
+
+ /**
+ * Gets a record by its primary key.
+ *
+ * @param mixed $primaryKey the primaryKey value
+ * @param array $options options given in this parameter are passed to elasticsearch
+ * as request URI parameters.
+ * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html)
+ * for more details on these options.
+ * @return static|null The record instance or null if it was not found.
+ */
+ public static function get($primaryKey, $options = [])
+ {
+ if ($primaryKey === null) {
+ return null;
+ }
+ $command = static::getDb()->createCommand();
+ $result = $command->get(static::index(), static::type(), $primaryKey, $options);
+ if ($result['exists']) {
+ return static::create($result);
+ }
+ return null;
+ }
+
+ /**
+ * Gets a list of records by its primary keys.
+ *
+ * @param array $primaryKeys an array of primaryKey values
+ * @param array $options options given in this parameter are passed to elasticsearch
+ * as request URI parameters.
+ *
+ * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html)
+ * for more details on these options.
+ * @return static|null The record instance or null if it was not found.
+ */
+
+ public static function mget($primaryKeys, $options = [])
+ {
+ if (empty($primaryKeys)) {
+ return [];
+ }
+ $command = static::getDb()->createCommand();
+ $result = $command->mget(static::index(), static::type(), $primaryKeys, $options);
+ $models = [];
+ foreach($result['docs'] as $doc) {
+ if ($doc['exists']) {
+ $models[] = static::create($doc);
+ }
+ }
+ return $models;
+ }
+
+ // TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html
+
+ // TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createQuery()
+ {
+ return new ActiveQuery(['modelClass' => get_called_class()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createActiveRelation($config = [])
+ {
+ return new ActiveRelation($config);
+ }
+
+ // TODO implement copy and move as pk change is not possible
+
+ public function getId()
+ {
+ return $this->_id;
+ }
+
+ /**
+ * Sets the primary key
+ * @param mixed $value
+ * @throws \yii\base\InvalidCallException when record is not new
+ */
+ public function setId($value)
+ {
+ if ($this->isNewRecord) {
+ $this->_id = $value;
+ } else {
+ throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPrimaryKey($asArray = false)
+ {
+ if ($asArray) {
+ return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id];
+ } else {
+ return $this->_id;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOldPrimaryKey($asArray = false)
+ {
+ $id = $this->isNewRecord ? null : $this->_id;
+ if ($asArray) {
+ return [ActiveRecord::PRIMARY_KEY_NAME => $id];
+ } else {
+ return $this->_id;
+ }
+ }
+
+ /**
+ * This method defines the primary.
+ *
+ * The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed.
+ *
+ * @return string[] the primary keys of this record.
+ */
+ public static function primaryKey()
+ {
+ return [ActiveRecord::PRIMARY_KEY_NAME];
+ }
+
+ /**
+ * Returns the list of all attribute names of the model.
+ * This method must be overridden by child classes to define available attributes.
+ * @return array list of attribute names.
+ */
+ public function attributes()
+ {
+ throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.');
+ }
+
+ /**
+ * @return string the name of the index this record is stored in.
+ */
+ public static function index()
+ {
+ return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
+ }
+
+ /**
+ * @return string the name of the type of this record.
+ */
+ public static function type()
+ {
+ return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
+ }
+
+ /**
+ * Creates an active record object using a row of data.
+ * This method is called by [[ActiveQuery]] to populate the query results
+ * into Active Records. It is not meant to be used to create new records.
+ * @param array $row attribute values (name => value)
+ * @return ActiveRecord the newly created active record.
+ */
+ public static function create($row)
+ {
+ $row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id'];
+ $record = parent::create($row['_source']);
+ return $record;
+ }
+
+ /**
+ * Inserts a document into the associated index using the attribute values of this record.
+ *
+ * This method performs the following steps in order:
+ *
+ * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
+ * fails, it will skip the rest of the steps;
+ * 2. call [[afterValidate()]] when `$runValidation` is true.
+ * 3. call [[beforeSave()]]. If the method returns false, it will skip the
+ * rest of the steps;
+ * 4. insert the record into database. If this fails, it will skip the rest of the steps;
+ * 5. call [[afterSave()]];
+ *
+ * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
+ * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
+ * will be raised by the corresponding methods.
+ *
+ * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
+ *
+ * If the [[primaryKey|primary key]] is not set (null) during insertion,
+ * it will be populated with a
+ * [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation)
+ * after insertion.
+ *
+ * For example, to insert a customer record:
+ *
+ * ~~~
+ * $customer = new Customer;
+ * $customer->name = $name;
+ * $customer->email = $email;
+ * $customer->insert();
+ * ~~~
+ *
+ * @param boolean $runValidation whether to perform validation before saving the record.
+ * If the validation fails, the record will not be inserted into the database.
+ * @param array $attributes list of attributes that need to be saved. Defaults to null,
+ * meaning all attributes will be saved.
+ * @param array $options options given in this parameter are passed to elasticsearch
+ * as request URI parameters. These are among others:
+ *
+ * - `routing` define shard placement of this record.
+ * - `parent` by giving the primaryKey of another record this defines a parent-child relation
+ * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time.
+ *
+ * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html)
+ * for more details on these options.
+ *
+ * By default the `op_type` is set to `create`.
+ * @return boolean whether the attributes are valid and the record is inserted successfully.
+ */
+ public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create'])
+ {
+ if ($runValidation && !$this->validate($attributes)) {
+ return false;
+ }
+ if ($this->beforeSave(true)) {
+ $values = $this->getDirtyAttributes($attributes);
+
+ $response = static::getDb()->createCommand()->insert(
+ static::index(),
+ static::type(),
+ $values,
+ $this->getPrimaryKey(),
+ $options
+ );
+
+ if (!$response['ok']) {
+ return false;
+ }
+ $this->_id = $response['_id'];
+ $this->_version = $response['_version'];
+ $this->setOldAttributes($values);
+ $this->afterSave(true);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Updates all records whos primary keys are given.
+ * For example, to change the status to be 1 for all customers whose status is 2:
+ *
+ * ~~~
+ * Customer::updateAll(array('status' => 1), array(2, 3, 4));
+ * ~~~
+ *
+ * @param array $attributes attribute values (name-value pairs) to be saved into the table
+ * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
+ * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ * @param array $params this parameter is ignored in elasticsearch implementation.
+ * @return integer the number of rows updated
+ */
+ public static function updateAll($attributes, $condition = [], $params = [])
+ {
+ if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) {
+ $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME];
+ } else {
+ $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME);
+ }
+ if (empty($primaryKeys)) {
+ return 0;
+ }
+ $bulk = '';
+ foreach((array) $primaryKeys as $pk) {
+ $action = Json::encode([
+ "update" => [
+ "_id" => $pk,
+ "_type" => static::type(),
+ "_index" => static::index(),
+ ],
+ ]);
+ $data = Json::encode(array(
+ "doc" => $attributes
+ ));
+ $bulk .= $action . "\n" . $data . "\n";
+ }
+
+ // TODO do this via command
+ $url = [static::index(), static::type(), '_bulk'];
+ $response = static::getDb()->post($url, [], $bulk);
+ $n=0;
+ foreach($response['items'] as $item) {
+ if ($item['update']['ok']) {
+ $n++;
+ }
+ }
+ return $n;
+ }
+
+
+ /**
+ * Deletes rows in the table using the provided conditions.
+ * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
+ *
+ * For example, to delete all customers whose status is 3:
+ *
+ * ~~~
+ * Customer::deleteAll('status = 3');
+ * ~~~
+ *
+ * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
+ * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
+ * @param array $params this parameter is ignored in elasticsearch implementation.
+ * @return integer the number of rows deleted
+ */
+ public static function deleteAll($condition = [], $params = [])
+ {
+ if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) {
+ $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME];
+ } else {
+ $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME);
+ }
+ if (empty($primaryKeys)) {
+ return 0;
+ }
+ $bulk = '';
+ foreach((array) $primaryKeys as $pk) {
+ $bulk .= Json::encode([
+ "delete" => [
+ "_id" => $pk,
+ "_type" => static::type(),
+ "_index" => static::index(),
+ ],
+ ]) . "\n";
+ }
+
+ // TODO do this via command
+ $url = [static::index(), static::type(), '_bulk'];
+ $response = static::getDb()->post($url, [], $bulk);
+ $n=0;
+ foreach($response['items'] as $item) {
+ if ($item['delete']['found'] && $item['delete']['ok']) {
+ $n++;
+ }
+ }
+ return $n;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function updateAllCounters($counters, $condition = null, $params = [])
+ {
+ throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getTableSchema()
+ {
+ throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function tableName()
+ {
+ return static::index() . '/' . static::type();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function findBySql($sql, $params = [])
+ {
+ throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.');
+ }
+
+ /**
+ * Returns a value indicating whether the specified operation is transactional in the current [[scenario]].
+ * This method will always return false as transactional operations are not supported by elasticsearch.
+ * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
+ * @return boolean whether the specified operation is transactional in the current [[scenario]].
+ */
+ public function isTransactional($operation)
+ {
+ return false;
+ }
+}
diff --git a/extensions/elasticsearch/ActiveRelation.php b/extensions/elasticsearch/ActiveRelation.php
new file mode 100644
index 0000000..a102697
--- /dev/null
+++ b/extensions/elasticsearch/ActiveRelation.php
@@ -0,0 +1,61 @@
+
+ * @since 2.0
+ */
+class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
+{
+ use ActiveRelationTrait;
+
+ /**
+ * Creates a DB command that can be used to execute this query.
+ * @param Connection $db the DB connection used to create the DB command.
+ * If null, the DB connection returned by [[modelClass]] will be used.
+ * @return Command the created DB command instance.
+ */
+ public function createCommand($db = null)
+ {
+ if ($this->primaryModel !== null) {
+ // lazy loading
+ if (is_array($this->via)) {
+ // via relation
+ /** @var ActiveRelation $viaQuery */
+ list($viaName, $viaQuery) = $this->via;
+ if ($viaQuery->multiple) {
+ $viaModels = $viaQuery->all();
+ $this->primaryModel->populateRelation($viaName, $viaModels);
+ } else {
+ $model = $viaQuery->one();
+ $this->primaryModel->populateRelation($viaName, $model);
+ $viaModels = $model === null ? [] : [$model];
+ }
+ $this->filterByModels($viaModels);
+ } else {
+ $this->filterByModels([$this->primaryModel]);
+ }
+ }
+ return parent::createCommand($db);
+ }
+}
diff --git a/extensions/elasticsearch/Command.php b/extensions/elasticsearch/Command.php
new file mode 100644
index 0000000..916d597
--- /dev/null
+++ b/extensions/elasticsearch/Command.php
@@ -0,0 +1,403 @@
+
+ * @since 2.0
+ */
+class Command extends Component
+{
+ /**
+ * @var Connection
+ */
+ public $db;
+ /**
+ * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index
+ */
+ public $index;
+ /**
+ * @var string|array the types to execute the query on. Defaults to null meaning all types
+ */
+ public $type;
+ /**
+ * @var array list of arrays or json strings that become parts of a query
+ */
+ public $queryParts;
+
+ public $options = [];
+
+ /**
+ * @param array $options
+ * @return mixed
+ */
+ public function search($options = [])
+ {
+ $query = $this->queryParts;
+ if (empty($query)) {
+ $query = '{}';
+ }
+ if (is_array($query)) {
+ $query = Json::encode($query);
+ }
+ $url = [
+ $this->index !== null ? $this->index : '_all',
+ $this->type !== null ? $this->type : '_all',
+ '_search'
+ ];
+ return $this->db->get($url, array_merge($this->options, $options), $query);
+ }
+
+ /**
+ * Inserts a document into an index
+ * @param string $index
+ * @param string $type
+ * @param string|array $data json string or array of data to store
+ * @param null $id the documents id. If not specified Id will be automatically choosen
+ * @param array $options
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html
+ */
+ public function insert($index, $type, $data, $id = null, $options = [])
+ {
+ $body = is_array($data) ? Json::encode($data) : $data;
+
+ if ($id !== null) {
+ return $this->db->put([$index, $type, $id], $options, $body);
+ } else {
+ return $this->db->post([$index, $type], $options, $body);
+ }
+ }
+
+ /**
+ * gets a document from the index
+ * @param $index
+ * @param $type
+ * @param $id
+ * @param array $options
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html
+ */
+ public function get($index, $type, $id, $options = [])
+ {
+ return $this->db->get([$index, $type, $id], $options, null, [200, 404]);
+ }
+
+ /**
+ * gets multiple documents from the index
+ *
+ * TODO allow specifying type and index + fields
+ * @param $index
+ * @param $type
+ * @param $ids
+ * @param array $options
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html
+ */
+ public function mget($index, $type, $ids, $options = [])
+ {
+ $body = Json::encode(['ids' => array_values($ids)]);
+ return $this->db->get([$index, $type, '_mget'], $options, $body);
+ }
+
+ /**
+ * gets a documents _source from the index (>=v0.90.1)
+ * @param $index
+ * @param $type
+ * @param $id
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source
+ */
+ public function getSource($index, $type, $id)
+ {
+ return $this->db->get([$index, $type, $id]);
+ }
+
+ /**
+ * gets a document from the index
+ * @param $index
+ * @param $type
+ * @param $id
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html
+ */
+ public function exists($index, $type, $id)
+ {
+ return $this->db->head([$index, $type, $id]);
+ }
+
+ /**
+ * deletes a document from the index
+ * @param $index
+ * @param $type
+ * @param $id
+ * @param array $options
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html
+ */
+ public function delete($index, $type, $id, $options = [])
+ {
+ return $this->db->delete([$index, $type, $id], $options);
+ }
+
+ /**
+ * updates a document
+ * @param $index
+ * @param $type
+ * @param $id
+ * @param array $options
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html
+ */
+// public function update($index, $type, $id, $data, $options = [])
+// {
+// // TODO implement
+//// return $this->db->delete([$index, $type, $id], $options);
+// }
+
+ // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html
+
+ /**
+ * creates an index
+ * @param $index
+ * @param array $configuration
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html
+ */
+ public function createIndex($index, $configuration = null)
+ {
+ $body = $configuration !== null ? Json::encode($configuration) : null;
+ return $this->db->put([$index], $body);
+ }
+
+ /**
+ * deletes an index
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html
+ */
+ public function deleteIndex($index)
+ {
+ return $this->db->delete([$index]);
+ }
+
+ /**
+ * deletes all indexes
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html
+ */
+ public function deleteAllIndexes()
+ {
+ return $this->db->delete(['_all']);
+ }
+
+ /**
+ * checks whether an index exists
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html
+ */
+ public function indexExists($index)
+ {
+ return $this->db->head([$index]);
+ }
+
+ /**
+ * @param $index
+ * @param $type
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html
+ */
+ public function typeExists($index, $type)
+ {
+ return $this->db->head([$index, $type]);
+ }
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html
+ */
+ public function openIndex($index)
+ {
+ return $this->db->post([$index, '_open']);
+ }
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html
+ */
+ public function closeIndex($index)
+ {
+ return $this->db->post([$index, '_close']);
+ }
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html
+ */
+ public function getIndexStatus($index = '_all')
+ {
+ return $this->db->get([$index, '_status']);
+ }
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html
+ // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html
+ */
+ public function clearIndexCache($index)
+ {
+ return $this->db->post([$index, '_cache', 'clear']);
+ }
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html
+ */
+ public function flushIndex($index = '_all')
+ {
+ return $this->db->post([$index, '_flush']);
+ }
+
+ /**
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html
+ */
+ public function refreshIndex($index)
+ {
+ return $this->db->post([$index, '_refresh']);
+ }
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html
+
+ /**
+ * @param $index
+ * @param $type
+ * @param $mapping
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html
+ */
+ public function setMapping($index, $type, $mapping)
+ {
+ $body = $mapping !== null ? Json::encode($mapping) : null;
+ return $this->db->put([$index, $type, '_mapping'], $body);
+ }
+
+ /**
+ * @param string $index
+ * @param string $type
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html
+ */
+ public function getMapping($index = '_all', $type = '_all')
+ {
+ return $this->db->get([$index, $type, '_mapping']);
+ }
+
+ /**
+ * @param $index
+ * @param $type
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html
+ */
+ public function deleteMapping($index, $type)
+ {
+ return $this->db->delete([$index, $type]);
+ }
+
+ /**
+ * @param $index
+ * @param string $type
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html
+ */
+ public function getFieldMapping($index, $type = '_all')
+ {
+ return $this->db->put([$index, $type, '_mapping']);
+ }
+
+ /**
+ * @param $options
+ * @param $index
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html
+ */
+// public function analyze($options, $index = null)
+// {
+// // TODO implement
+//// return $this->db->put([$index]);
+// }
+
+ /**
+ * @param $name
+ * @param $pattern
+ * @param $settings
+ * @param $mappings
+ * @param int $order
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html
+ */
+ public function createTemplate($name, $pattern, $settings, $mappings, $order = 0)
+ {
+ $body = Json::encode([
+ 'template' => $pattern,
+ 'order' => $order,
+ 'settings' => (object) $settings,
+ 'mappings' => (object) $mappings,
+ ]);
+ return $this->db->put(['_template', $name], $body);
+
+ }
+
+ /**
+ * @param $name
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html
+ */
+ public function deleteTemplate($name)
+ {
+ return $this->db->delete(['_template', $name]);
+
+ }
+
+ /**
+ * @param $name
+ * @return mixed
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html
+ */
+ public function getTemplate($name)
+ {
+ return $this->db->get(['_template', $name]);
+ }
+}
\ No newline at end of file
diff --git a/extensions/elasticsearch/Connection.php b/extensions/elasticsearch/Connection.php
new file mode 100644
index 0000000..d5275e8
--- /dev/null
+++ b/extensions/elasticsearch/Connection.php
@@ -0,0 +1,346 @@
+
+ * @since 2.0
+ */
+class Connection extends Component
+{
+ /**
+ * @event Event an event that is triggered after a DB connection is established
+ */
+ const EVENT_AFTER_OPEN = 'afterOpen';
+
+ /**
+ * @var bool whether to autodetect available cluster nodes on [[open()]]
+ */
+ public $autodetectCluster = true;
+ /**
+ * @var array cluster nodes
+ * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true.
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info
+ */
+ public $nodes = [
+ ['http_address' => 'inet[/127.0.0.1:9200]'],
+ ];
+ /**
+ * @var array the active node. key of [[nodes]]. Will be randomly selected on [[open()]].
+ */
+ public $activeNode;
+
+ // TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth
+ public $auth = [];
+ /**
+ * @var float timeout to use for connecting to an elasticsearch node.
+ * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option.
+ * If not set, no explicit timeout will be set for curl.
+ */
+ public $connectionTimeout = null;
+ /**
+ * @var float timeout to use when reading the response from an elasticsearch node.
+ * This value will be used to configure the curl `CURLOPT_TIMEOUT` option.
+ * If not set, no explicit timeout will be set for curl.
+ */
+ public $dataTimeout = null;
+
+
+ public function init()
+ {
+ foreach($this->nodes as $node) {
+ if (!isset($node['http_address'])) {
+ throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.');
+ }
+ }
+ }
+
+ /**
+ * Closes the connection when this component is being serialized.
+ * @return array
+ */
+ public function __sleep()
+ {
+ $this->close();
+ return array_keys(get_object_vars($this));
+ }
+
+ /**
+ * Returns a value indicating whether the DB connection is established.
+ * @return boolean whether the DB connection is established
+ */
+ public function getIsActive()
+ {
+ return $this->activeNode !== null;
+ }
+
+ /**
+ * Establishes a DB connection.
+ * It does nothing if a DB connection has already been established.
+ * @throws Exception if connection fails
+ */
+ public function open()
+ {
+ if ($this->activeNode !== null) {
+ return;
+ }
+ if (empty($this->nodes)) {
+ throw new InvalidConfigException('elasticsearch needs at least one node to operate.');
+ }
+ if ($this->autodetectCluster) {
+ $node = reset($this->nodes);
+ $host = $node['http_address'];
+ if (strncmp($host, 'inet[/', 6) == 0) {
+ $host = substr($host, 6, -1);
+ }
+ $response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes');
+ $this->nodes = $response['nodes'];
+ if (empty($this->nodes)) {
+ throw new Exception('cluster autodetection did not find any active node.');
+ }
+ }
+ $this->selectActiveNode();
+ Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes)
+ . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__);
+ $this->initConnection();
+ }
+
+ /**
+ * select active node randomly
+ */
+ protected function selectActiveNode()
+ {
+ $keys = array_keys($this->nodes);
+ $this->activeNode = $keys[rand(0, count($keys) - 1)];
+ }
+
+ /**
+ * Closes the currently active DB connection.
+ * It does nothing if the connection is already closed.
+ */
+ public function close()
+ {
+ Yii::trace('Closing connection to elasticsearch. Active node was: '
+ . $this->nodes[$this->activeNode]['http_address'], __CLASS__);
+ $this->activeNode = null;
+ }
+
+ /**
+ * Initializes the DB connection.
+ * This method is invoked right after the DB connection is established.
+ * The default implementation triggers an [[EVENT_AFTER_OPEN]] event.
+ */
+ protected function initConnection()
+ {
+ $this->trigger(self::EVENT_AFTER_OPEN);
+ }
+
+ /**
+ * Returns the name of the DB driver for the current [[dsn]].
+ * @return string name of the DB driver
+ */
+ public function getDriverName()
+ {
+ return 'elasticsearch';
+ }
+
+ /**
+ * Creates a command for execution.
+ * @param array $config the configuration for the Command class
+ * @return Command the DB command
+ */
+ public function createCommand($config = [])
+ {
+ $this->open();
+ $config['db'] = $this;
+ $command = new Command($config);
+ return $command;
+ }
+
+ public function getQueryBuilder()
+ {
+ return new QueryBuilder($this);
+ }
+
+ public function get($url, $options = [], $body = null)
+ {
+ $this->open();
+ return $this->httpRequest('GET', $this->createUrl($url, $options), $body);
+ }
+
+ public function head($url, $options = [], $body = null)
+ {
+ $this->open();
+ return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body);
+ }
+
+ public function post($url, $options = [], $body = null)
+ {
+ $this->open();
+ return $this->httpRequest('POST', $this->createUrl($url, $options), $body);
+ }
+
+ public function put($url, $options = [], $body = null)
+ {
+ $this->open();
+ return $this->httpRequest('PUT', $this->createUrl($url, $options), $body);
+ }
+
+ public function delete($url, $options = [], $body = null)
+ {
+ $this->open();
+ return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body);
+ }
+
+ private function createUrl($path, $options = [])
+ {
+ $url = implode('/', array_map(function($a) {
+ return urlencode(is_array($a) ? implode(',', $a) : $a);
+ }, $path));
+
+ if (!empty($options)) {
+ $url .= '?' . http_build_query($options);
+ }
+ return [$this->nodes[$this->activeNode]['http_address'], $url];
+ }
+
+ protected function httpRequest($method, $url, $requestBody = null)
+ {
+ $method = strtoupper($method);
+
+ // response body and headers
+ $headers = [];
+ $body = '';
+
+ $options = [
+ CURLOPT_USERAGENT => 'Yii2 Framework ' . __CLASS__,
+ CURLOPT_RETURNTRANSFER => false,
+ CURLOPT_HEADER => false,
+ // http://www.php.net/manual/en/function.curl-setopt.php#82418
+ CURLOPT_HTTPHEADER => ['Expect:'],
+
+ CURLOPT_WRITEFUNCTION => function($curl, $data) use (&$body) {
+ $body .= $data;
+ return mb_strlen($data, '8bit');
+ },
+ CURLOPT_HEADERFUNCTION => function($curl, $data) use (&$headers) {
+ foreach(explode("\r\n", $data) as $row) {
+ if (($pos = strpos($row, ':')) !== false) {
+ $headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1));
+ }
+ }
+ return mb_strlen($data, '8bit');
+ },
+ CURLOPT_CUSTOMREQUEST => $method,
+ ];
+ if ($this->connectionTimeout !== null) {
+ $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout;
+ }
+ if ($this->dataTimeout !== null) {
+ $options[CURLOPT_TIMEOUT] = $this->dataTimeout;
+ }
+ if ($requestBody !== null) {
+ $options[CURLOPT_POSTFIELDS] = $requestBody;
+ }
+ if ($method == 'HEAD') {
+ $options[CURLOPT_NOBODY] = true;
+ unset($options[CURLOPT_WRITEFUNCTION]);
+ }
+
+ if (is_array($url)) {
+ list($host, $q) = $url;
+ if (strncmp($host, 'inet[/', 6) == 0) {
+ $host = substr($host, 6, -1);
+ }
+ $profile = $q . $requestBody;
+ $url = 'http://' . $host . '/' . $q;
+ } else {
+ $profile = false;
+ }
+
+ Yii::trace("Sending request to elasticsearch node: $url\n$requestBody", __METHOD__);
+ if ($profile !== false) {
+ Yii::beginProfile($profile, __METHOD__);
+ }
+
+ $curl = curl_init($url);
+ curl_setopt_array($curl, $options);
+ if (curl_exec($curl) === false) {
+ throw new Exception('Elasticsearch request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [
+ 'requestMethod' => $method,
+ 'requestUrl' => $url,
+ 'requestBody' => $requestBody,
+ 'responseHeaders' => $headers,
+ 'responseBody' => $body,
+ ]);
+ }
+
+ $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close($curl);
+
+ if ($profile !== false) {
+ Yii::endProfile($profile, __METHOD__);
+ }
+
+ if ($responseCode >= 200 && $responseCode < 300) {
+ if ($method == 'HEAD') {
+ return true;
+ } else {
+ if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) {
+ throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [
+ 'requestMethod' => $method,
+ 'requestUrl' => $url,
+ 'requestBody' => $requestBody,
+ 'responseCode' => $responseCode,
+ 'responseHeaders' => $headers,
+ 'responseBody' => $body,
+ ]);
+ }
+ if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) {
+ return Json::decode($body);
+ }
+ throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [
+ 'requestMethod' => $method,
+ 'requestUrl' => $url,
+ 'requestBody' => $requestBody,
+ 'responseCode' => $responseCode,
+ 'responseHeaders' => $headers,
+ 'responseBody' => $body,
+ ]);
+ }
+ } elseif ($responseCode == 404) {
+ return false;
+ } else {
+ throw new Exception("Elasticsearch request failed with code $responseCode.", [
+ 'requestMethod' => $method,
+ 'requestUrl' => $url,
+ 'requestBody' => $requestBody,
+ 'responseCode' => $responseCode,
+ 'responseHeaders' => $headers,
+ 'responseBody' => $body,
+ ]);
+ }
+ }
+
+ public function getNodeInfo()
+ {
+ return $this->get([]);
+ }
+
+ public function getClusterState()
+ {
+ return $this->get(['_cluster', 'state']);
+ }
+}
\ No newline at end of file
diff --git a/extensions/elasticsearch/Exception.php b/extensions/elasticsearch/Exception.php
new file mode 100644
index 0000000..aa58338
--- /dev/null
+++ b/extensions/elasticsearch/Exception.php
@@ -0,0 +1,43 @@
+
+ * @since 2.0
+ */
+class Exception extends \yii\db\Exception
+{
+ /**
+ * @var array additional information about the http request that caused the error.
+ */
+ public $errorInfo = [];
+
+ /**
+ * Constructor.
+ * @param string $message error message
+ * @param array $errorInfo error info
+ * @param integer $code error code
+ * @param \Exception $previous The previous exception used for the exception chaining.
+ */
+ public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null)
+ {
+ $this->errorInfo = $errorInfo;
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * @return string the user-friendly name of this exception
+ */
+ public function getName()
+ {
+ return \Yii::t('yii', 'Elasticsearch Database Exception');
+ }
+}
\ No newline at end of file
diff --git a/extensions/elasticsearch/LICENSE.md b/extensions/elasticsearch/LICENSE.md
new file mode 100644
index 0000000..e98f03d
--- /dev/null
+++ b/extensions/elasticsearch/LICENSE.md
@@ -0,0 +1,32 @@
+The Yii framework is free software. It is released under the terms of
+the following BSD License.
+
+Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+ * Neither the name of Yii Software LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/extensions/elasticsearch/Query.php b/extensions/elasticsearch/Query.php
new file mode 100644
index 0000000..7da9051
--- /dev/null
+++ b/extensions/elasticsearch/Query.php
@@ -0,0 +1,506 @@
+fields('id, name')
+ * ->from('myindex', 'users')
+ * ->limit(10);
+ * // build and execute the query
+ * $command = $query->createCommand();
+ * $rows = $command->search(); // this way you get the raw output of elasticsearch.
+ * ~~~
+ *
+ * You would normally call `$query->search()` instead of creating a command as this method
+ * adds the `indexBy()` feature and also removes some inconsistencies from the response.
+ *
+ * Query also provides some methods to easier get some parts of the result only:
+ *
+ * - [[one()]]: returns a single record populated with the first row of data.
+ * - [[all()]]: returns all records based on the query results.
+ * - [[count()]]: returns the number of records.
+ * - [[scalar()]]: returns the value of the first column in the first row of the query result.
+ * - [[column()]]: returns the value of the first column in the query result.
+ * - [[exists()]]: returns a value indicating whether the query result has data or not.
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class Query extends Component implements QueryInterface
+{
+ use QueryTrait;
+
+ /**
+ * @var array the fields being retrieved from the documents. For example, `['id', 'name']`.
+ * If not set, it means retrieving all fields. An empty array will result in no fields being
+ * retrieved. This means that only the primaryKey of a record will be available in the result.
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields
+ * @see fields()
+ */
+ public $fields;
+ /**
+ * @var string|array The index to retrieve data from. This can be a string representing a single index
+ * or a an array of multiple indexes. If this is not set, indexes are being queried.
+ * @see from()
+ */
+ public $index;
+ /**
+ * @var string|array The type to retrieve data from. This can be a string representing a single type
+ * or a an array of multiple types. If this is not set, all types are being queried.
+ * @see from()
+ */
+ public $type;
+ /**
+ * @var integer A search timeout, bounding the search request to be executed within the specified time value
+ * and bail with the hits accumulated up to that point when expired. Defaults to no timeout.
+ * @see timeout()
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3
+ */
+ public $timeout;
+ /**
+ * @var array|string The query part of this search query. This is an array or json string that follows the format of
+ * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html).
+ */
+ public $query;
+ /**
+ * @var array|string The filter part of this search query. This is an array or json string that follows the format of
+ * the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html).
+ */
+ public $filter;
+
+ public $facets = [];
+
+ public function init()
+ {
+ parent::init();
+ // setting the default limit according to elasticsearch defaults
+ // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3
+ if ($this->limit === null) {
+ $this->limit = 10;
+ }
+ }
+
+ /**
+ * Creates a DB command that can be used to execute this query.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return Command the created DB command instance.
+ */
+ public function createCommand($db = null)
+ {
+ if ($db === null) {
+ $db = Yii::$app->getComponent('elasticsearch');
+ }
+
+ $commandConfig = $db->getQueryBuilder()->build($this);
+ return $db->createCommand($commandConfig);
+ }
+
+ /**
+ * Executes the query and returns all results as an array.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return array the query results. If the query results in nothing, an empty array will be returned.
+ */
+ public function all($db = null)
+ {
+ $result = $this->createCommand($db)->search();
+ if (empty($result['hits']['hits'])) {
+ return [];
+ }
+ $rows = $result['hits']['hits'];
+ if ($this->indexBy === null && $this->fields === null) {
+ return $rows;
+ }
+ $models = [];
+ foreach ($rows as $key => $row) {
+ if ($this->fields !== null) {
+ $row['_source'] = isset($row['fields']) ? $row['fields'] : [];
+ unset($row['fields']);
+ }
+ if ($this->indexBy !== null) {
+ if (is_string($this->indexBy)) {
+ $key = $row['_source'][$this->indexBy];
+ } else {
+ $key = call_user_func($this->indexBy, $row);
+ }
+ }
+ $models[$key] = $row;
+ }
+ return $models;
+ }
+
+ /**
+ * Executes the query and returns a single row of result.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query
+ * results in nothing.
+ */
+ public function one($db = null)
+ {
+ $options['size'] = 1;
+ $result = $this->createCommand($db)->search($options);
+ if (empty($result['hits']['hits'])) {
+ return false;
+ }
+ $record = reset($result['hits']['hits']);
+ if ($this->fields !== null) {
+ $record['_source'] = isset($record['fields']) ? $record['fields'] : [];
+ unset($record['fields']);
+ }
+ return $record;
+ }
+
+ /**
+ * Executes the query and returns the complete search result including e.g. hits, facets, totalCount.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @param array $options The options given with this query. Possible options are:
+ * - [routing](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-routing)
+ * - [search_type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html)
+ * @return array the query results.
+ */
+ public function search($db = null, $options = [])
+ {
+ $result = $this->createCommand($db)->search($options);
+ if (!empty($result['hits']['hits']) && ($this->indexBy === null || $this->fields === null)) {
+ $rows = [];
+ foreach ($result['hits']['hits'] as $key => $row) {
+ if ($this->fields !== null) {
+ $row['_source'] = isset($row['fields']) ? $row['fields'] : [];
+ unset($row['fields']);
+ }
+ if ($this->indexBy !== null) {
+ if (is_string($this->indexBy)) {
+ $key = $row['_source'][$this->indexBy];
+ } else {
+ $key = call_user_func($this->indexBy, $row);
+ }
+ }
+ $rows[$key] = $row;
+ }
+ $result['hits']['hits'] = $rows;
+ }
+ return $result;
+ }
+
+ // TODO add query stats http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#stats-groups
+
+ // TODO add scroll/scan http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan
+
+ /**
+ * Executes the query and deletes all matching documents.
+ *
+ * This will not run facet queries.
+ *
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return array the query results. If the query results in nothing, an empty array will be returned.
+ */
+ public function delete($db = null)
+ {
+ // TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html
+ throw new NotSupportedException('Delete by query is not implemented yet.');
+ }
+
+ /**
+ * Returns the query result as a scalar value.
+ * The value returned will be the specified field in the first document of the query results.
+ * @param string $field name of the attribute to select
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return string the value of the specified attribute in the first record of the query result.
+ * Null is returned if the query result is empty or the field does not exist.
+ */
+ public function scalar($field, $db = null)
+ {
+ $record = self::one($db); // TODO limit fields to the one required
+ if ($record !== false && isset($record['_source'][$field])) {
+ return $record['_source'][$field];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Executes the query and returns the first column of the result.
+ * @param string $field the field to query over
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return array the first column of the query result. An empty array is returned if the query results in nothing.
+ */
+ public function column($field, $db = null)
+ {
+ $command = $this->createCommand($db);
+ $command->queryParts['fields'] = [$field];
+ $result = $command->search();
+ if (empty($result['hits']['hits'])) {
+ return [];
+ }
+ $column = [];
+ foreach ($result['hits']['hits'] as $row) {
+ $column[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null;
+ }
+ return $column;
+ }
+
+ /**
+ * Returns the number of records.
+ * @param string $q the COUNT expression. This parameter is ignored by this implementation.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return integer number of records
+ */
+ public function count($q = '*', $db = null)
+ {
+ // TODO consider sending to _count api instead of _search for performance
+ // only when no facety are registerted.
+ // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html
+
+ $options = [];
+ $options['search_type'] = 'count';
+ $count = $this->createCommand($db)->search($options)['hits']['total'];
+ if ($this->limit === null && $this->offset === null) {
+ return $count;
+ } elseif ($this->offset !== null) {
+ $count = $this->offset < $count ? $count - $this->offset : 0;
+ }
+ return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit);
+ }
+
+ /**
+ * Returns a value indicating whether the query result contains any row of data.
+ * @param Connection $db the database connection used to execute the query.
+ * If this parameter is not given, the `elasticsearch` application component will be used.
+ * @return boolean whether the query result contains any row of data.
+ */
+ public function exists($db = null)
+ {
+ return self::one($db) !== false;
+ }
+
+ /**
+ * Adds a facet search to this query.
+ * @param string $name the name of this facet
+ * @param string $type the facet type. e.g. `terms`, `range`, `histogram`...
+ * @param string|array $options the configuration options for this facet. Can be an array or a json string.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html
+ */
+ public function addFacet($name, $type, $options)
+ {
+ $this->facets[$name] = [$type => $options];
+ return $this;
+ }
+
+ /**
+ * The `terms facet` allow to specify field facets that return the N most frequent terms.
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html
+ */
+ public function addTermFacet($name, $options)
+ {
+ return $this->addFacet($name, 'terms', $options);
+ }
+
+ /**
+ * Range facet allows to specify a set of ranges and get both the number of docs (count) that fall
+ * within each range, and aggregated data either based on the field, or using another field.
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html
+ */
+ public function addRangeFacet($name, $options)
+ {
+ return $this->addFacet($name, 'range', $options);
+ }
+
+ /**
+ * The histogram facet works with numeric data by building a histogram across intervals of the field values.
+ * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per
+ * interval/bucket (count and total).
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html
+ */
+ public function addHistogramFacet($name, $options)
+ {
+ return $this->addFacet($name, 'histogram', $options);
+ }
+
+ /**
+ * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet.
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html
+ */
+ public function addDateHistogramFacet($name, $options)
+ {
+ return $this->addFacet($name, 'date_histogram', $options);
+ }
+
+ /**
+ * A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter.
+ * The filter itself can be expressed using the Query DSL.
+ * @param string $name the name of this facet
+ * @param string $filter the query in Query DSL
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html
+ */
+ public function addFilterFacet($name, $filter)
+ {
+ return $this->addFacet($name, 'filter', $filter);
+ }
+
+ /**
+ * A facet query allows to return a count of the hits matching the facet query.
+ * The query itself can be expressed using the Query DSL.
+ * @param string $name the name of this facet
+ * @param string $query the query in Query DSL
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html
+ */
+ public function addQueryFacet($name, $query)
+ {
+ return $this->addFacet($name, 'query', $query);
+ }
+
+ /**
+ * Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count,
+ * total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation.
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html
+ */
+ public function addStatisticalFacet($name, $options)
+ {
+ return $this->addFacet($name, 'statistical', $options);
+ }
+
+ /**
+ * The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field,
+ * per term value driven by another field.
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html
+ */
+ public function addTermsStatsFacet($name, $options)
+ {
+ return $this->addFacet($name, 'terms_stats', $options);
+ }
+
+ /**
+ * The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point`
+ * including count of the number of hits that fall within each range, and aggregation information (like `total`).
+ * @param string $name the name of this facet
+ * @param array $options additional option. Please refer to the elasticsearch documentation for details.
+ * @return static
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html
+ */
+ public function addGeoDistanceFacet($name, $options)
+ {
+ return $this->addFacet($name, 'geo_distance', $options);
+ }
+
+ // TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html
+
+ // TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html
+
+ // TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html
+
+ /**
+ * Sets the querypart of this search query.
+ * @param string $query
+ * @return static
+ */
+ public function query($query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Sets the filter part of this search query.
+ * @param string $filter
+ * @return static
+ */
+ public function filter($filter)
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * Sets the index and type to retrieve documents from.
+ * @param string|array $index The index to retrieve data from. This can be a string representing a single index
+ * or a an array of multiple indexes. If this is `null` it means that all indexes are being queried.
+ * @param string|array $type The type to retrieve data from. This can be a string representing a single type
+ * or a an array of multiple types. If this is `null` it means that all types are being queried.
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type
+ */
+ public function from($index, $type = null)
+ {
+ $this->index = $index;
+ $this->type = $type;
+ }
+
+ /**
+ * Sets the fields to retrieve from the documents.
+ * @param array $fields the fields to be selected.
+ * @return static the query object itself
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html
+ */
+ public function fields($fields)
+ {
+ if (is_array($fields) || $fields === null) {
+ $this->fields = $fields;
+ } else {
+ $this->fields = func_get_args();
+ }
+ return $this;
+ }
+
+ /**
+ * Sets the search timeout.
+ * @param integer $timeout A search timeout, bounding the search request to be executed within the specified time value
+ * and bail with the hits accumulated up to that point when expired. Defaults to no timeout.
+ * @return static the query object itself
+ * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3
+ */
+ public function timeout($timeout)
+ {
+ $this->timeout = $timeout;
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/extensions/elasticsearch/QueryBuilder.php b/extensions/elasticsearch/QueryBuilder.php
new file mode 100644
index 0000000..5ed77e6
--- /dev/null
+++ b/extensions/elasticsearch/QueryBuilder.php
@@ -0,0 +1,349 @@
+
+ * @since 2.0
+ */
+class QueryBuilder extends \yii\base\Object
+{
+ /**
+ * @var Connection the database connection.
+ */
+ public $db;
+
+ /**
+ * Constructor.
+ * @param Connection $connection the database connection.
+ * @param array $config name-value pairs that will be used to initialize the object properties
+ */
+ public function __construct($connection, $config = [])
+ {
+ $this->db = $connection;
+ parent::__construct($config);
+ }
+
+ /**
+ * Generates query from a [[Query]] object.
+ * @param Query $query the [[Query]] object from which the query will be generated
+ * @return array the generated SQL statement (the first array element) and the corresponding
+ * parameters to be bound to the SQL statement (the second array element).
+ */
+ public function build($query)
+ {
+ $parts = [];
+
+ if ($query->fields !== null) {
+ $parts['fields'] = (array) $query->fields;
+ }
+ if ($query->limit !== null && $query->limit >= 0) {
+ $parts['size'] = $query->limit;
+ }
+ if ($query->offset > 0) {
+ $parts['from'] = (int) $query->offset;
+ }
+
+ if (empty($parts['query'])) {
+ $parts['query'] = ["match_all" => (object)[]];
+ }
+
+ $whereFilter = $this->buildCondition($query->where);
+ if (is_string($query->filter)) {
+ if (empty($whereFilter)) {
+ $parts['filter'] = $query->filter;
+ } else {
+ $parts['filter'] = '{"and": [' . $query->filter . ', ' . Json::encode($whereFilter) . ']}';
+ }
+ } elseif ($query->filter !== null) {
+ if (empty($whereFilter)) {
+ $parts['filter'] = $query->filter;
+ } else {
+ $parts['filter'] = ['and' => [$query->filter, $whereFilter]];
+ }
+ } elseif (!empty($whereFilter)) {
+ $parts['filter'] = $whereFilter;
+ }
+
+ $sort = $this->buildOrderBy($query->orderBy);
+ if (!empty($sort)) {
+ $parts['sort'] = $sort;
+ }
+
+ if (!empty($query->facets)) {
+ $parts['facets'] = $query->facets;
+ }
+
+ $options = [];
+ if ($query->timeout !== null) {
+ $options['timeout'] = $query->timeout;
+ }
+
+ return [
+ 'queryParts' => $parts,
+ 'index' => $query->index,
+ 'type' => $query->type,
+ 'options' => $options,
+ ];
+ }
+
+ /**
+ * adds order by condition to the query
+ */
+ public function buildOrderBy($columns)
+ {
+ if (empty($columns)) {
+ return [];
+ }
+ $orders = [];
+ foreach ($columns as $name => $direction) {
+ if (is_string($direction)) {
+ $column = $direction;
+ $direction = SORT_ASC;
+ } else {
+ $column = $name;
+ }
+ if ($column == ActiveRecord::PRIMARY_KEY_NAME) {
+ $column = '_uid';
+ }
+
+ // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/
+ if (is_array($direction)) {
+ $orders[] = [$column => $direction];
+ } else {
+ $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')];
+ }
+ }
+ return $orders;
+ }
+
+ /**
+ * Parses the condition specification and generates the corresponding SQL expression.
+ * @param string|array $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
+ * @throws \yii\db\Exception if the condition is in bad format
+ */
+ public function buildCondition($condition)
+ {
+ static $builders = array(
+ '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',
+ );
+
+ if (empty($condition)) {
+ return [];
+ }
+ if (!is_array($condition)) {
+ throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.');
+ }
+ if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
+ $operator = strtolower($condition[0]);
+ if (isset($builders[$operator])) {
+ $method = $builders[$operator];
+ array_shift($condition);
+ return $this->$method($operator, $condition);
+ } else {
+ throw new InvalidParamException('Found unknown operator in query: ' . $operator);
+ }
+ } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
+ return $this->buildHashCondition($condition);
+ }
+ }
+
+ private function buildHashCondition($condition)
+ {
+ $parts = [];
+ foreach($condition as $attribute => $value) {
+ if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) {
+ if ($value == null) { // there is no null pk
+ $parts[] = ['script' => ['script' => '0==1']];
+ } else {
+ $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]];
+ }
+ } else {
+ if (is_array($value)) { // IN condition
+ $parts[] = ['in' => [$attribute => $value]];
+ } else {
+ if ($value === null) {
+ $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]];
+ } else {
+ $parts[] = ['term' => [$attribute => $value]];
+ }
+ }
+ }
+ }
+ return count($parts) === 1 ? $parts[0] : ['and' => $parts];
+ }
+
+ private function buildAndCondition($operator, $operands)
+ {
+ $parts = [];
+ foreach ($operands as $operand) {
+ if (is_array($operand)) {
+ $operand = $this->buildCondition($operand);
+ }
+ if (!empty($operand)) {
+ $parts[] = $operand;
+ }
+ }
+ if (!empty($parts)) {
+ return [$operator => $parts];
+ } else {
+ return [];
+ }
+ }
+
+ private function buildBetweenCondition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1], $operands[2])) {
+ throw new InvalidParamException("Operator '$operator' requires three operands.");
+ }
+
+ list($column, $value1, $value2) = $operands;
+ if ($column == ActiveRecord::PRIMARY_KEY_NAME) {
+ throw new NotSupportedException('Between condition is not supported for primaryKey.');
+ }
+ $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]];
+ if ($operator == 'not between') {
+ $filter = ['not' => $filter];
+ }
+ return $filter;
+ }
+
+ private function buildInCondition($operator, $operands)
+ {
+ if (!isset($operands[0], $operands[1])) {
+ throw new InvalidParamException("Operator '$operator' requires two operands.");
+ }
+
+ list($column, $values) = $operands;
+
+ $values = (array)$values;
+
+ if (empty($values) || $column === []) {
+ return $operator === 'in' ? ['script' => ['script' => '0==1']] : [];
+ }
+
+ if (count($column) > 1) {
+ return $this->buildCompositeInCondition($operator, $column, $values, $params);
+ } elseif (is_array($column)) {
+ $column = reset($column);
+ }
+ $canBeNull = false;
+ foreach ($values as $i => $value) {
+ if (is_array($value)) {
+ $values[$i] = $value = isset($value[$column]) ? $value[$column] : null;
+ }
+ if ($value === null) {
+ $canBeNull = true;
+ unset($values[$i]);
+ }
+ }
+ if ($column == ActiveRecord::PRIMARY_KEY_NAME) {
+ if (empty($values) && $canBeNull) { // there is no null pk
+ $filter = ['script' => ['script' => '0==1']];
+ } else {
+ $filter = ['ids' => ['values' => array_values($values)]];
+ if ($canBeNull) {
+ $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]];
+ }
+ }
+ } else {
+ if (empty($values) && $canBeNull) {
+ $filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]];
+ } else {
+ $filter = ['in' => [$column => array_values($values)]];
+ if ($canBeNull) {
+ $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]];
+ }
+ }
+ }
+ if ($operator == 'not in') {
+ $filter = ['not' => $filter];
+ }
+ return $filter;
+ }
+
+ protected function buildCompositeInCondition($operator, $columns, $values)
+ {
+ throw new NotSupportedException('composite in is not supported by elasticsearch.');
+ $vss = array();
+ foreach ($values as $value) {
+ $vs = array();
+ 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) . ')';
+ }
+ foreach ($columns as $i => $column) {
+ if (strpos($column, '(') === false) {
+ $columns[$i] = $this->db->quoteColumnName($column);
+ }
+ }
+ return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')';
+ }
+
+ private function buildLikeCondition($operator, $operands)
+ {
+ throw new NotSupportedException('like conditions is not supported by elasticsearch.');
+ if (!isset($operands[0], $operands[1])) {
+ throw new Exception("Operator '$operator' requires two operands.");
+ }
+
+ list($column, $values) = $operands;
+
+ $values = (array)$values;
+
+ if (empty($values)) {
+ return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0==1' : '';
+ }
+
+ if ($operator === 'LIKE' || $operator === 'NOT LIKE') {
+ $andor = ' AND ';
+ } else {
+ $andor = ' OR ';
+ $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE';
+ }
+
+ if (strpos($column, '(') === false) {
+ $column = $this->db->quoteColumnName($column);
+ }
+
+ $parts = array();
+ foreach ($values as $value) {
+ $phName = self::PARAM_PREFIX . count($params);
+ $params[$phName] = $value;
+ $parts[] = "$column $operator $phName";
+ }
+
+ return implode($andor, $parts);
+ }
+}
diff --git a/extensions/elasticsearch/README.md b/extensions/elasticsearch/README.md
new file mode 100644
index 0000000..57497e2
--- /dev/null
+++ b/extensions/elasticsearch/README.md
@@ -0,0 +1,148 @@
+Elasticsearch Query and ActiveRecord for Yii 2
+==============================================
+
+This extension provides the [elasticsearch](http://www.elasticsearch.org/) integration for the Yii2 framework.
+It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active
+records in elasticsearch.
+
+To use this extension, you have to configure the Connection class in your application configuration:
+
+```php
+return [
+ //....
+ 'components' => [
+ 'elasticsearch' => [
+ 'class' => 'yii\elasticsearch\Connection',
+ 'nodes' => [
+ ['http_address' => '127.0.0.1:9200'],
+ // configure more hosts if you have a cluster
+ ],
+ ],
+ ]
+];
+```
+
+
+Installation
+------------
+
+The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+
+Either run
+
+```
+php composer.phar require yiisoft/yii2-elasticsearch "*"
+```
+
+or add
+
+```json
+"yiisoft/yii2-elasticsearch": "*"
+```
+
+to the require section of your composer.json.
+
+
+Using the Query
+---------------
+
+TBD
+
+Using the ActiveRecord
+----------------------
+
+For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md).
+
+For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and
+implement at least the `attributes()` method to define the attributes of the record.
+The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()` and can not be changed.
+The primary key is not part of the attributes.
+
+ primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified.
+The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do
+not specify your own primary key.
+
+The following is an example model called `Customer`:
+
+```php
+class Customer extends \yii\elasticsearch\ActiveRecord
+{
+ /**
+ * @return array the list of attributes for this record
+ */
+ public function attributes()
+ {
+ return ['id', 'name', 'address', 'registration_date'];
+ }
+
+ /**
+ * @return ActiveRelation defines a relation to the Order record (can be in other database, e.g. redis or sql)
+ */
+ public function getOrders()
+ {
+ return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id');
+ }
+
+ /**
+ * Defines a scope that modifies the `$query` to return only active(status = 1) customers
+ */
+ public static function active($query)
+ {
+ $query->andWhere(array('status' => 1));
+ }
+}
+```
+
+You may override [[index()]] and [[type()]] to define the index and type this record represents.
+
+The general usage of elasticsearch ActiveRecord is very similar to the database ActiveRecord as described in the
+[guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md).
+It supports the same interface and features except the following limitations and additions(*!*):
+
+- As elasticsearch does not support SQL, the query API does not support `join()`, `groupBy()`, `having()` and `union()`.
+ Sorting, limit, offset and conditional where are all supported.
+- `from()` does not select the tables, but the [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index)
+ and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against.
+- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology.
+ It defines the fields to retrieve from a document.
+- `via`-relations can not be defined via a table as there are not tables in elasticsearch. You can only define relations via other records.
+- As elasticsearch is a data storage and search engine there is of course support added for search your records.
+ There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query.
+ See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html)
+ on how to compose `query` and `filter` parts.
+- It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa.
+
+Elasticsearch separates primary key from attributes. You need to set the `id` property of the record to set its primary key.
+
+Usage example:
+
+```php
+$customer = new Customer();
+$customer->id = 1;
+$customer->attributes = ['name' => 'test'];
+$customer->save();
+
+$customer = Customer::get(1); // get a record by pk
+$customers = Customer::get([1,2,3]); // get a records multiple by pk
+$customer = Customer::find()->where(['name' => 'test'])->one(); // find by query
+$customers = Customer::find()->active()->all(); // find all by query (using the `active` scope)
+
+// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-field-query.html
+$result = Article::find()->query(["field" => ["title" => "yii"]])->all(); // articles whose title contains "yii"
+
+// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-flt-query.html
+$query = Article::find()->query([
+ "fuzzy_like_this" => [
+ "fields" => ["title", "description"],
+ "like_text" => "This query will return articles that are similar to this text :-)",
+ "max_query_terms" : 12
+ ]
+]);
+
+$query->all(); // gives you all the documents
+// you can add facets to your search:
+$query->addStatisticalFacet('click_stats', ['field' => 'visit_count']);
+$query->search(); // gives you all the records + stats about the visit_count field. e.g. mean, sum, min, max etc...
+```
+
+And there is so much more in it. "it’s endless what you can build"[¹](http://www.elasticsearch.org/)
diff --git a/extensions/elasticsearch/composer.json b/extensions/elasticsearch/composer.json
new file mode 100644
index 0000000..c72cd81
--- /dev/null
+++ b/extensions/elasticsearch/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "yiisoft/yii2-elasticsearch",
+ "description": "Elasticsearch integration and ActiveRecord for the Yii framework",
+ "keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"],
+ "type": "yii2-extension",
+ "license": "BSD-3-Clause",
+ "support": {
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aelasticsearch",
+ "forum": "http://www.yiiframework.com/forum/",
+ "wiki": "http://www.yiiframework.com/wiki/",
+ "irc": "irc://irc.freenode.net/yii",
+ "source": "https://github.com/yiisoft/yii2"
+ },
+ "authors": [
+ {
+ "name": "Carsten Brandt",
+ "email": "mail@cebe.cc"
+ }
+ ],
+ "require": {
+ "yiisoft/yii2": "*",
+ "ext-curl": "*"
+ },
+ "autoload": {
+ "psr-0": { "yii\\elasticsearch\\": "" }
+ },
+ "target-dir": "yii/elasticsearch"
+}
diff --git a/extensions/gii/Generator.php b/extensions/gii/Generator.php
index 05c45a7..53e54bb 100644
--- a/extensions/gii/Generator.php
+++ b/extensions/gii/Generator.php
@@ -63,7 +63,7 @@ abstract class Generator extends Model
abstract public function generate();
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -164,7 +164,7 @@ abstract class Generator extends Model
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*
* Child classes should override this method like the following so that the parent
* rules are included:
diff --git a/extensions/gii/GiiAsset.php b/extensions/gii/GiiAsset.php
index b100750..64dc62b 100644
--- a/extensions/gii/GiiAsset.php
+++ b/extensions/gii/GiiAsset.php
@@ -18,25 +18,25 @@ use yii\web\AssetBundle;
class GiiAsset extends AssetBundle
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $sourcePath = '@yii/gii/assets';
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $css = [
'main.css',
'typeahead.js-bootstrap.css',
];
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $js = [
'gii.js',
'typeahead.js',
];
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $depends = [
'yii\web\YiiAsset',
diff --git a/extensions/gii/Module.php b/extensions/gii/Module.php
index 7dc9590..5644e29 100644
--- a/extensions/gii/Module.php
+++ b/extensions/gii/Module.php
@@ -54,7 +54,7 @@ use yii\web\HttpException;
class Module extends \yii\base\Module
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $controllerNamespace = 'yii\gii\controllers';
/**
@@ -92,7 +92,7 @@ class Module extends \yii\base\Module
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -103,7 +103,7 @@ class Module extends \yii\base\Module
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function beforeAction($action)
{
diff --git a/extensions/gii/composer.json b/extensions/gii/composer.json
index 8654621..4e17844 100644
--- a/extensions/gii/composer.json
+++ b/extensions/gii/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Agii",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/gii/generators/controller/Generator.php b/extensions/gii/generators/controller/Generator.php
index 08b29d5..769db0a 100644
--- a/extensions/gii/generators/controller/Generator.php
+++ b/extensions/gii/generators/controller/Generator.php
@@ -38,7 +38,7 @@ class Generator extends \yii\gii\Generator
public $actions = 'index';
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
@@ -47,7 +47,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -55,7 +55,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getDescription()
{
@@ -64,7 +64,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -79,7 +79,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
@@ -92,7 +92,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function requiredTemplates()
{
@@ -103,7 +103,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function stickyAttributes()
{
@@ -111,7 +111,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function hints()
{
@@ -134,7 +134,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function successMessage()
{
@@ -149,7 +149,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function generate()
{
diff --git a/extensions/gii/generators/crud/Generator.php b/extensions/gii/generators/crud/Generator.php
index 1925de2..107c8f2 100644
--- a/extensions/gii/generators/crud/Generator.php
+++ b/extensions/gii/generators/crud/Generator.php
@@ -69,7 +69,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function hints()
{
@@ -95,7 +95,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function stickyAttributes()
{
@@ -123,7 +123,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function generate()
{
diff --git a/extensions/gii/generators/crud/templates/search.php b/extensions/gii/generators/crud/templates/search.php
index 1411896..17a0024 100644
--- a/extensions/gii/generators/crud/templates/search.php
+++ b/extensions/gii/generators/crud/templates/search.php
@@ -40,7 +40,7 @@ class = $searchModelClass ?> extends Model
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
diff --git a/extensions/gii/generators/form/Generator.php b/extensions/gii/generators/form/Generator.php
index 3bc0be6..cc4328e 100644
--- a/extensions/gii/generators/form/Generator.php
+++ b/extensions/gii/generators/form/Generator.php
@@ -26,7 +26,7 @@ class Generator extends \yii\gii\Generator
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -34,7 +34,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getDescription()
{
@@ -42,7 +42,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function generate()
{
@@ -55,7 +55,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -72,7 +72,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
@@ -85,7 +85,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function requiredTemplates()
{
@@ -93,7 +93,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function stickyAttributes()
{
@@ -101,7 +101,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function hints()
{
@@ -114,7 +114,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function successMessage()
{
diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php
index cd2fcbf..8c505f1 100644
--- a/extensions/gii/generators/model/Generator.php
+++ b/extensions/gii/generators/model/Generator.php
@@ -32,7 +32,7 @@ class Generator extends \yii\gii\Generator
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -40,7 +40,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getDescription()
{
@@ -48,7 +48,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -68,7 +68,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
@@ -84,7 +84,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function hints()
{
@@ -111,7 +111,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function autoCompleteData()
{
@@ -128,7 +128,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function requiredTemplates()
{
@@ -136,7 +136,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function stickyAttributes()
{
@@ -144,7 +144,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function generate()
{
diff --git a/extensions/gii/generators/model/templates/model.php b/extensions/gii/generators/model/templates/model.php
index dcd1461..555bee1 100644
--- a/extensions/gii/generators/model/templates/model.php
+++ b/extensions/gii/generators/model/templates/model.php
@@ -33,7 +33,7 @@ namespace = $generator->ns ?>;
class = $className ?> extends = '\\' . ltrim($generator->baseClass, '\\') . "\n" ?>
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function tableName()
{
@@ -41,7 +41,7 @@ class = $className ?> extends = '\\' . ltrim($generator->baseClass, '\\') .
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -49,7 +49,7 @@ class = $className ?> extends = '\\' . ltrim($generator->baseClass, '\\') .
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
diff --git a/extensions/gii/generators/module/Generator.php b/extensions/gii/generators/module/Generator.php
index 5946e07..a29555b 100644
--- a/extensions/gii/generators/module/Generator.php
+++ b/extensions/gii/generators/module/Generator.php
@@ -24,7 +24,7 @@ class Generator extends \yii\gii\Generator
public $moduleID;
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getName()
{
@@ -32,7 +32,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getDescription()
{
@@ -40,7 +40,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function rules()
{
@@ -54,7 +54,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attributeLabels()
{
@@ -65,7 +65,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function hints()
{
@@ -76,7 +76,7 @@ class Generator extends \yii\gii\Generator
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function successMessage()
{
@@ -104,7 +104,7 @@ EOD;
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function requiredTemplates()
{
@@ -112,7 +112,7 @@ EOD;
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function generate()
{
diff --git a/extensions/gii/views/layouts/main.php b/extensions/gii/views/layouts/main.php
index 983475b..788b10c 100644
--- a/extensions/gii/views/layouts/main.php
+++ b/extensions/gii/views/layouts/main.php
@@ -9,16 +9,16 @@ use yii\helpers\Html;
*/
$asset = yii\gii\GiiAsset::register($this);
?>
-beginPage(); ?>
+beginPage() ?>
= Html::encode($this->title) ?>
- head(); ?>
+ head() ?>
-beginBody(); ?>
+beginBody() ?>
Html::img($asset->baseUrl . '/logo.png'),
@@ -47,7 +47,7 @@ NavBar::end();
-endBody(); ?>
+endBody() ?>
-endPage(); ?>
+endPage() ?>
diff --git a/extensions/jui/composer.json b/extensions/jui/composer.json
index ff54422..a8b9559 100644
--- a/extensions/jui/composer.json
+++ b/extensions/jui/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Ajui",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php
index 2174901..755fc6f 100644
--- a/extensions/redis/ActiveQuery.php
+++ b/extensions/redis/ActiveQuery.php
@@ -226,7 +226,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface
{
$record = $this->one($db);
if ($record !== null) {
- return $record->$attribute;
+ return $record->hasAttribute($attribute) ? $record->$attribute : null;
} else {
return null;
}
diff --git a/extensions/redis/ActiveRecord.php b/extensions/redis/ActiveRecord.php
index 46132fc..8c2933c 100644
--- a/extensions/redis/ActiveRecord.php
+++ b/extensions/redis/ActiveRecord.php
@@ -48,7 +48,7 @@ class ActiveRecord extends \yii\db\ActiveRecord
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public static function createQuery()
{
@@ -56,7 +56,7 @@ class ActiveRecord extends \yii\db\ActiveRecord
}
/**
- * @inheritDoc
+ * {@inheritdoc}
*/
public static function createActiveRelation($config = [])
{
@@ -81,13 +81,13 @@ class ActiveRecord extends \yii\db\ActiveRecord
* This method must be overridden by child classes to define available attributes.
* @return array list of attribute names.
*/
- public static function attributes()
+ public function attributes()
{
throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.');
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
public function insert($runValidation = true, $attributes = null)
{
@@ -294,19 +294,19 @@ class ActiveRecord extends \yii\db\ActiveRecord
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function getTableSchema()
{
- throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord');
+ throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord.');
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function findBySql($sql, $params = [])
{
- throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord');
+ throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord.');
}
/**
diff --git a/extensions/redis/Cache.php b/extensions/redis/Cache.php
index a7a535c..627acdc 100644
--- a/extensions/redis/Cache.php
+++ b/extensions/redis/Cache.php
@@ -30,18 +30,28 @@ use yii\base\InvalidConfigException;
* 'components' => [
* 'cache' => [
* 'class' => 'yii\redis\Cache',
+ * 'redis' => [
+ * 'hostname' => 'localhost',
+ * 'port' => 6379,
+ * 'database' => 0,
+ * ]
* ],
- * 'redis' => [
- * 'class' => 'yii\redis\Connection',
- * 'hostname' => 'localhost',
- * 'port' => 6379,
- * 'database' => 0,
- * ]
* ],
* ]
* ~~~
*
- * @property Connection $connection The redis connection object. This property is read-only.
+ * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient:
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'cache' => [
+ * 'class' => 'yii\redis\Cache',
+ * // 'redis' => 'redis' // id of the connection application component
+ * ],
+ * ],
+ * ]
+ * ~~~
*
* @author Carsten Brandt
* @since 2.0
@@ -49,7 +59,9 @@ use yii\base\InvalidConfigException;
class Cache extends \yii\caching\Cache
{
/**
- * @var Connection|string the Redis [[Connection]] object or the application component ID of the Redis [[Connection]].
+ * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]].
+ * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure
+ * redis connection as an application component.
* After the Cache object is created, if you want to change this property, you should only assign it
* with a Redis [[Connection]] object.
*/
@@ -57,15 +69,20 @@ class Cache extends \yii\caching\Cache
/**
- * Initializes the DbCache component.
- * This method will initialize the [[db]] property to make sure it refers to a valid DB connection.
- * @throws InvalidConfigException if [[db]] is invalid.
+ * Initializes the redis Cache component.
+ * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ * @throws InvalidConfigException if [[redis]] is invalid.
*/
public function init()
{
parent::init();
if (is_string($this->redis)) {
$this->redis = Yii::$app->getComponent($this->redis);
+ } else if (is_array($this->redis)) {
+ if (!isset($this->redis['class'])) {
+ $this->redis['class'] = Connection::className();
+ }
+ $this->redis = Yii::createObject($this->redis);
}
if (!$this->redis instanceof Connection) {
throw new InvalidConfigException("Cache::redis must be either a Redis connection instance or the application component ID of a Redis connection.");
@@ -88,7 +105,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function getValue($key)
{
@@ -96,7 +113,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function getValues($keys)
{
@@ -110,7 +127,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function setValue($key, $value, $expire)
{
@@ -123,7 +140,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function setValues($data, $expire)
{
@@ -157,7 +174,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function addValue($key, $value, $expire)
{
@@ -170,7 +187,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function deleteValue($key)
{
@@ -178,7 +195,7 @@ class Cache extends \yii\caching\Cache
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
protected function flushValues()
{
diff --git a/extensions/redis/README.md b/extensions/redis/README.md
index 28cecf1..af85678 100644
--- a/extensions/redis/README.md
+++ b/extensions/redis/README.md
@@ -1,9 +1,9 @@
-Redis Cache and ActiveRecord for Yii 2
-======================================
+Redis Cache, Session and ActiveRecord for Yii 2
+===============================================
This extension provides the [redis](http://redis.io/) key-value store support for the Yii2 framework.
-It includes a `Cache` class and implents the `ActiveRecord` pattern that allows you to store active
-records in redis.
+It includes a `Cache` and `Session` storage handler and implents the `ActiveRecord` pattern that allows
+you to store active records in redis.
To use this extension, you have to configure the Connection class in your application configuration:
@@ -21,7 +21,31 @@ return [
];
```
-To use the `Cache` component, you also have to configure the `cache` component to be `yii\redis\Cache`:
+Installation
+------------
+
+The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+
+Either run
+
+```
+php composer.phar require yiisoft/yii2-redis "*"
+```
+
+or add
+
+```json
+"yiisoft/yii2-redis": "*"
+```
+
+to the require section of your composer.json.
+
+
+Using the Cache component
+-------------------------
+
+To use the `Cache` component, in addtition to configuring the connection as described above,
+you also have to configure the `cache` component to be `yii\redis\Cache`:
```php
return [
@@ -35,26 +59,64 @@ return [
];
```
+If you only use the redis cache, you can also configure the parameters of the connection within the
+cache component (no connection application component needs to be configured in this case):
-Installation
-------------
+```php
+return [
+ //....
+ 'components' => [
+ // ...
+ 'cache' => [
+ 'class' => 'yii\redis\Cache',
+ 'redis' => [
+ 'hostname' => 'localhost',
+ 'port' => 6379,
+ 'database' => 0,
+ ],
+ ],
+ ]
+];
+```
-The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+Using the Session component
+---------------------------
-Either run
+To use the `Session` component, in addtition to configuring the connection as described above,
+you also have to configure the `session` component to be `yii\redis\Session`:
-```
-php composer.phar require yiisoft/yii2-redis "*"
+```php
+return [
+ //....
+ 'components' => [
+ // ...
+ 'session' => [
+ 'class' => 'yii\redis\Session',
+ ],
+ ]
+];
```
-or add
+If you only use the redis session, you can also configure the parameters of the connection within the
+cache component (no connection application component needs to be configured in this case):
-```json
-"yiisoft/yii2-redis": "*"
+```php
+return [
+ //....
+ 'components' => [
+ // ...
+ 'session' => [
+ 'class' => 'yii\redis\Session',
+ 'redis' => [
+ 'hostname' => 'localhost',
+ 'port' => 6379,
+ 'database' => 0,
+ ],
+ ],
+ ]
+];
```
-to the require section of your composer.json.
-
Using the redis ActiveRecord
----------------------------
@@ -72,10 +134,29 @@ The following is an example model called `Customer`:
```php
class Customer extends \yii\redis\ActiveRecord
{
- public function attributes()
- {
- return ['id', 'name', 'address', 'registration_date'];
- }
+ /**
+ * @return array the list of attributes for this record
+ */
+ public function attributes()
+ {
+ return ['id', 'name', 'address', 'registration_date'];
+ }
+
+ /**
+ * @return ActiveRelation defines a relation to the Order record (can be in other database, e.g. elasticsearch or sql)
+ */
+ public function getOrders()
+ {
+ return $this->hasMany(Order::className(), ['customer_id' => 'id']);
+ }
+
+ /**
+ * Defines a scope that modifies the `$query` to return only active(status = 1) customers
+ */
+ public static function active($query)
+ {
+ $query->andWhere(array('status' => 1));
+ }
}
```
@@ -88,4 +169,16 @@ It supports the same interface and features except the following limitations:
(orderBy() is not yet implemented: [#1305](https://github.com/yiisoft/yii2/issues/1305))
- `via`-relations can not be defined via a table as there are not tables in redis. You can only define relations via other records.
-It is also possible to define relations from redis ActiveRecords to normal ActiveRecord classes and vice versa.
\ No newline at end of file
+It is also possible to define relations from redis ActiveRecords to normal ActiveRecord classes and vice versa.
+
+Usage example:
+
+```php
+$customer = new Customer();
+$customer->attributes = ['name' => 'test'];
+$customer->save();
+echo $customer->id; // id will automatically be incremented if not set explicitly
+
+$customer = Customer::find()->where(['name' => 'test'])->one(); // find by query
+$customer = Customer::find()->active()->all(); // find all by query (using the `active` scope)
+```
diff --git a/extensions/redis/Session.php b/extensions/redis/Session.php
new file mode 100644
index 0000000..8f7a7e5
--- /dev/null
+++ b/extensions/redis/Session.php
@@ -0,0 +1,157 @@
+ [
+ * 'session' => [
+ * 'class' => 'yii\redis\Session',
+ * 'redis' => [
+ * 'hostname' => 'localhost',
+ * 'port' => 6379,
+ * 'database' => 0,
+ * ]
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * Or if you have configured the redis [[Connection]] as an application component, the following is sufficient:
+ *
+ * ~~~
+ * [
+ * 'components' => [
+ * 'session' => [
+ * 'class' => 'yii\redis\Session',
+ * // 'redis' => 'redis' // id of the connection application component
+ * ],
+ * ],
+ * ]
+ * ~~~
+ *
+ * @property boolean $useCustomStorage Whether to use custom storage. This property is read-only.
+ *
+ * @author Carsten Brandt
+ * @since 2.0
+ */
+class Session extends \yii\web\Session
+{
+ /**
+ * @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]].
+ * This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure
+ * redis connection as an application component.
+ * After the Session object is created, if you want to change this property, you should only assign it
+ * with a Redis [[Connection]] object.
+ */
+ public $redis = 'redis';
+ /**
+ * @var string a string prefixed to every cache key so that it is unique. If not set,
+ * it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string
+ * if you don't want to use key prefix. It is recommended that you explicitly set this property to some
+ * static value if the cached data needs to be shared among multiple applications.
+ *
+ * To ensure interoperability, only alphanumeric characters should be used.
+ */
+ public $keyPrefix;
+
+
+ /**
+ * Initializes the redis Session component.
+ * This method will initialize the [[redis]] property to make sure it refers to a valid redis connection.
+ * @throws InvalidConfigException if [[redis]] is invalid.
+ */
+ public function init()
+ {
+ if (is_string($this->redis)) {
+ $this->redis = Yii::$app->getComponent($this->redis);
+ } else if (is_array($this->redis)) {
+ if (!isset($this->redis['class'])) {
+ $this->redis['class'] = Connection::className();
+ }
+ $this->redis = Yii::createObject($this->redis);
+ }
+ if (!$this->redis instanceof Connection) {
+ throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection.");
+ }
+ if ($this->keyPrefix === null) {
+ $this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
+ } elseif (!ctype_alnum($this->keyPrefix)) {
+ throw new InvalidConfigException(get_class($this) . '::keyPrefix should only contain alphanumeric characters.');
+ }
+ parent::init();
+ }
+
+ /**
+ * Returns a value indicating whether to use custom session storage.
+ * This method overrides the parent implementation and always returns true.
+ * @return boolean whether to use custom storage.
+ */
+ public function getUseCustomStorage()
+ {
+ return true;
+ }
+
+ /**
+ * Session read handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @return string the session data
+ */
+ public function readSession($id)
+ {
+ $data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]);
+ return $data === false ? '' : $data;
+ }
+
+ /**
+ * Session write handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @param string $data session data
+ * @return boolean whether session write is successful
+ */
+ public function writeSession($id, $data)
+ {
+ return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]);
+ }
+
+ /**
+ * Session destroy handler.
+ * Do not call this method directly.
+ * @param string $id session ID
+ * @return boolean whether session is destroyed successfully
+ */
+ public function destroySession($id)
+ {
+ return (bool) $this->redis->executeCommand('DEL', [$this->calculateKey($id)]);
+ }
+
+ /**
+ * Generates a unique key used for storing session data in cache.
+ * @param string $id session variable name
+ * @return string a safe cache key associated with the session variable name
+ */
+ protected function calculateKey($id)
+ {
+ return $this->keyPrefix . md5(json_encode([__CLASS__, $id]));
+ }
+}
diff --git a/extensions/redis/composer.json b/extensions/redis/composer.json
index fb5065b..16807ae 100644
--- a/extensions/redis/composer.json
+++ b/extensions/redis/composer.json
@@ -1,7 +1,7 @@
{
"name": "yiisoft/yii2-redis",
- "description": "Redis Cache and ActiveRecord for the Yii framework",
- "keywords": ["yii", "redis", "active-record", "cache"],
+ "description": "Redis Cache, Session and ActiveRecord for the Yii framework",
+ "keywords": ["yii", "redis", "active-record", "cache", "session"],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
diff --git a/extensions/smarty/composer.json b/extensions/smarty/composer.json
index 88b75a3..a3a8254 100644
--- a/extensions/smarty/composer.json
+++ b/extensions/smarty/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Asmarty",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/sphinx/ActiveQuery.php b/extensions/sphinx/ActiveQuery.php
index 62a6ef0..7ba48d2 100644
--- a/extensions/sphinx/ActiveQuery.php
+++ b/extensions/sphinx/ActiveQuery.php
@@ -176,7 +176,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function defaultConnection()
{
diff --git a/extensions/sphinx/ActiveRecord.php b/extensions/sphinx/ActiveRecord.php
index d83db62..9cf03ff 100644
--- a/extensions/sphinx/ActiveRecord.php
+++ b/extensions/sphinx/ActiveRecord.php
@@ -26,11 +26,16 @@ use Yii;
* read-only.
* @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]].
* @property array $oldAttributes The old attribute values (name-value pairs).
- * @property integer $oldPrimaryKey The old primary key value. This property is read-only.
+ * @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
+ * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
+ * value is null). This property is read-only.
* @property array $populatedRelations An array of relation data indexed by relation names. This property is
* read-only.
- * @property integer $primaryKey The primary key value. This property is read-only.
- * @property string $snippet current snippet value for this Active Record instance..
+ * @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if
+ * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
+ * This property is read-only.
+ * @property string $snippet Snippet value.
+ * @property string $snippetSource Snippet source string. This property is read-only.
*
* @author Paul Klimov
* @since 2.0
@@ -421,7 +426,7 @@ abstract class ActiveRecord extends Model
* This method is overridden so that attributes and related objects can be accessed like properties.
* @param string $name property name
* @return mixed property value
- * @see getAttribute
+ * @see getAttribute()
*/
public function __get($name)
{
@@ -493,7 +498,7 @@ abstract class ActiveRecord extends Model
/**
* Declares a `has-one` relation.
- * The declaration is returned in terms of an [[ActiveRelationInterface]] instance
+ * The declaration is returned in terms of an [[ActiveRelation]] instance
* through which the related record can be queried and retrieved back.
*
* A `has-one` relation means that there is at most one related record matching
@@ -513,21 +518,18 @@ abstract class ActiveRecord extends Model
* in the related class `ArticleContent`, while the 'id' value refers to an attribute name
* in the current AR class.
*
- * @param string $type relation type or class name.
- * - if value contains backslash ("\"), it is treated as full active relation class name,
- * for example: "app\mydb\ActiveRelation"
- * - if value does not contain backslash ("\"), the active relation class name will be composed
- * by pattern: "yii\{type}\ActiveRelation", for example: type "db" refers "yii\db\ActiveRelation",
- * type "sphinx" - "yii\sphinx\ActiveRelation"
+ * Call methods declared in [[ActiveRelation]] to further customize the relation.
+ *
* @param string $class the class name of the related record
* @param array $link the primary-foreign key constraint. The keys of the array refer to
* the attributes in the `$class` model, while the values of the array refer to the corresponding
* attributes in the index associated with this AR class.
* @return ActiveRelationInterface the relation object.
*/
- public function hasOne($type, $class, $link)
+ public function hasOne($class, $link)
{
- return $this->createActiveRelation($type, [
+ /** @var ActiveRecord $class */
+ return $class::createActiveRelation([
'modelClass' => $class,
'primaryModel' => $this,
'link' => $link,
@@ -557,21 +559,16 @@ abstract class ActiveRecord extends Model
* an attribute name in the related class `Tag`, while the 'tag_id' value refers to
* a multi value attribute name in the current AR class.
*
- * @param string $type relation type or class name.
- * - if value contains backslash ("\"), it is treated as full active relation class name,
- * for example: "app\mydb\ActiveRelation"
- * - if value does not contain backslash ("\"), the active relation class name will be composed
- * by pattern: "yii\{type}\ActiveRelation", for example: type "db" refers "yii\db\ActiveRelation",
- * type "sphinx" - "yii\sphinx\ActiveRelation"
* @param string $class the class name of the related record
* @param array $link the primary-foreign key constraint. The keys of the array refer to
* the columns in the table associated with the `$class` model, while the values of the
* array refer to the corresponding columns in the table associated with this AR class.
* @return ActiveRelationInterface the relation object.
*/
- public function hasMany($type, $class, $link)
+ public function hasMany($class, $link)
{
- return $this->createActiveRelation($type, [
+ /** @var ActiveRecord $class */
+ return $class::createActiveRelation([
'modelClass' => $class,
'primaryModel' => $this,
'link' => $link,
@@ -583,19 +580,12 @@ abstract class ActiveRecord extends Model
* Creates an [[ActiveRelationInterface]] instance.
* This method is called by [[hasOne()]] and [[hasMany()]] to create a relation instance.
* You may override this method to return a customized relation.
- * @param string $type relation type or class name.
* @param array $config the configuration passed to the ActiveRelation class.
* @return ActiveRelationInterface the newly created [[ActiveRelation]] instance.
*/
- protected function createActiveRelation($type, $config = [])
+ public static function createActiveRelation($config = [])
{
- if (strpos($type, '\\') === false) {
- $class = "yii\\{$type}\\ActiveRelation";
- } else {
- $class = $type;
- }
- $config['class'] = $class;
- return Yii::createObject($config);
+ return new ActiveRelation($config);
}
/**
@@ -635,7 +625,7 @@ abstract class ActiveRecord extends Model
*/
public function attributes()
{
- return array_keys($this->getIndexSchema()->columns);
+ return array_keys(static::getIndexSchema()->columns);
}
/**
@@ -645,7 +635,7 @@ abstract class ActiveRecord extends Model
*/
public function hasAttribute($name)
{
- return isset($this->_attributes[$name]) || isset($this->getIndexSchema()->columns[$name]);
+ return isset($this->_attributes[$name]) || in_array($name, $this->attributes());
}
/**
@@ -654,7 +644,7 @@ abstract class ActiveRecord extends Model
* null will be returned.
* @param string $name the attribute name
* @return mixed the attribute value. Null if the attribute is not set or does not exist.
- * @see hasAttribute
+ * @see hasAttribute()
*/
public function getAttribute($name)
{
@@ -666,7 +656,7 @@ abstract class ActiveRecord extends Model
* @param string $name the attribute name
* @param mixed $value the attribute value.
* @throws InvalidParamException if the named attribute does not exist.
- * @see hasAttribute
+ * @see hasAttribute()
*/
public function setAttribute($name, $value)
{
@@ -703,7 +693,7 @@ abstract class ActiveRecord extends Model
* @param string $name the attribute name
* @return mixed the old attribute value. Null if the attribute is not loaded before
* or does not exist.
- * @see hasAttribute
+ * @see hasAttribute()
*/
public function getOldAttribute($name)
{
@@ -715,7 +705,7 @@ abstract class ActiveRecord extends Model
* @param string $name the attribute name
* @param mixed $value the old attribute value.
* @throws InvalidParamException if the named attribute does not exist.
- * @see hasAttribute
+ * @see hasAttribute()
*/
public function setOldAttribute($name, $value)
{
@@ -1091,7 +1081,7 @@ abstract class ActiveRecord extends Model
/**
* Sets the value indicating whether the record is new.
* @param boolean $value whether the record is new and should be inserted when calling [[save()]].
- * @see getIsNewRecord
+ * @see getIsNewRecord()
*/
public function setIsNewRecord($value)
{
@@ -1215,7 +1205,7 @@ abstract class ActiveRecord extends Model
return false;
}
foreach ($this->attributes() as $name) {
- $this->_attributes[$name] = $record->_attributes[$name];
+ $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
}
$this->_oldAttributes = $this->_attributes;
$this->_related = [];
@@ -1225,20 +1215,28 @@ abstract class ActiveRecord extends Model
/**
* Returns a value indicating whether the given active record is the same as the current one.
* The comparison is made by comparing the index names and the primary key values of the two active records.
+ * If one of the records [[isNewRecord|is new]] they are also considered not equal.
* @param ActiveRecord $record record to compare to
* @return boolean whether the two active records refer to the same row in the same index.
*/
public function equals($record)
{
+ if ($this->isNewRecord || $record->isNewRecord) {
+ return false;
+ }
return $this->indexName() === $record->indexName() && $this->getPrimaryKey() === $record->getPrimaryKey();
}
/**
- * Returns the primary key value.
+ * Returns the primary key value(s).
* @param boolean $asArray whether to return the primary key value as an array. If true,
* the return value will be an array with column names as keys and column values as values.
- * @return mixed the primary key value. An array (column name => column value) is returned
- * if `$asArray` is true. A string is returned otherwise (null will be returned if
+ * Note that for composite primary keys, an array will always be returned regardless of this parameter value.
+ * @property mixed The primary key value. An array (column name => column value) is returned if
+ * the primary key is composite. A string is returned otherwise (null will be returned if
+ * the key value is null).
+ * @return mixed the primary key value. An array (column name => column value) is returned if the primary key
+ * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
* the key value is null).
*/
public function getPrimaryKey($asArray = false)
@@ -1256,15 +1254,18 @@ abstract class ActiveRecord extends Model
}
/**
- * Returns the old primary key value.
+ * Returns the old primary key value(s).
* This refers to the primary key value that is populated into the record
* after executing a find method (e.g. find(), findAll()).
* The value remains unchanged even if the primary key attribute is manually assigned with a different value.
* @param boolean $asArray whether to return the primary key value as an array. If true,
* the return value will be an array with column name as key and column value as value.
- * If this is false (default), a scalar value will be returned.
- * @return mixed the old primary key value. An array (column name => column value) is returned if
- * `$asArray` is true. A string is returned otherwise (null will be returned if
+ * If this is false (default), a scalar value will be returned for non-composite primary key.
+ * @property mixed The old primary key value. An array (column name => column value) is
+ * returned if the primary key is composite. A string is returned otherwise (null will be
+ * returned if the key value is null).
+ * @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
+ * is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
* the key value is null).
*/
public function getOldPrimaryKey($asArray = false)
@@ -1348,7 +1349,7 @@ abstract class ActiveRecord extends Model
if ($relation instanceof ActiveRelationInterface) {
return $relation;
} else {
- return null;
+ throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
}
} catch (UnknownMethodException $e) {
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
diff --git a/extensions/sphinx/Command.php b/extensions/sphinx/Command.php
index 93c02f8..a6c8c4f 100644
--- a/extensions/sphinx/Command.php
+++ b/extensions/sphinx/Command.php
@@ -39,14 +39,17 @@ use yii\base\NotSupportedException;
*
* To build SELECT SQL statements, please use [[Query]] and [[QueryBuilder]] instead.
*
- * @property \yii\sphinx\Connection $db the Sphinx connection that this command is associated with.
- *
* @author Paul Klimov
* @since 2.0
*/
class Command extends \yii\db\Command
{
/**
+ * @var \yii\sphinx\Connection the Sphinx connection that this command is associated with.
+ */
+ public $db;
+
+ /**
* Creates a batch INSERT command.
* For example,
*
@@ -194,7 +197,7 @@ class Command extends \yii\db\Command
// Not Supported :
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function createTable($table, $columns, $options = null)
{
@@ -202,7 +205,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function renameTable($table, $newName)
{
@@ -210,7 +213,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function dropTable($table)
{
@@ -218,7 +221,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function truncateTable($table)
{
@@ -226,7 +229,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function addColumn($table, $column, $type)
{
@@ -234,7 +237,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function dropColumn($table, $column)
{
@@ -242,7 +245,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function renameColumn($table, $oldName, $newName)
{
@@ -250,7 +253,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function alterColumn($table, $column, $type)
{
@@ -258,7 +261,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function addPrimaryKey($name, $table, $columns)
{
@@ -266,7 +269,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function dropPrimaryKey($name, $table)
{
@@ -274,7 +277,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
{
@@ -282,7 +285,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function dropForeignKey($name, $table)
{
@@ -290,7 +293,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function createIndex($name, $table, $columns, $unique = false)
{
@@ -298,7 +301,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function dropIndex($name, $table)
{
@@ -306,7 +309,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function resetSequence($table, $value = null)
{
@@ -314,7 +317,7 @@ class Command extends \yii\db\Command
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function checkIntegrity($check = true, $schema = '')
{
diff --git a/extensions/sphinx/Connection.php b/extensions/sphinx/Connection.php
index a43b0c2..a3492bb 100644
--- a/extensions/sphinx/Connection.php
+++ b/extensions/sphinx/Connection.php
@@ -47,11 +47,14 @@ use yii\base\NotSupportedException;
*
* Note: while this class extends "yii\db\Connection" some of its methods are not supported.
*
+ * @method \yii\sphinx\Schema getSchema() The schema information for this Sphinx connection
+ * @method \yii\sphinx\QueryBuilder getQueryBuilder() the query builder for this Sphinx connection
+ *
+ * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
+ * sequence object. This property is read-only.
* @property Schema $schema The schema information for this Sphinx connection. This property is read-only.
* @property \yii\sphinx\QueryBuilder $queryBuilder The query builder for this Sphinx connection. This property is
* read-only.
- * @method \yii\sphinx\Schema getSchema() The schema information for this Sphinx connection
- * @method \yii\sphinx\QueryBuilder getQueryBuilder() the query builder for this Sphinx connection
*
* @author Paul Klimov
* @since 2.0
@@ -59,7 +62,7 @@ use yii\base\NotSupportedException;
class Connection extends \yii\db\Connection
{
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public $schemaMap = [
'mysqli' => 'yii\sphinx\Schema', // MySQL
diff --git a/extensions/sphinx/Query.php b/extensions/sphinx/Query.php
index ff0dcba..dd13363 100644
--- a/extensions/sphinx/Query.php
+++ b/extensions/sphinx/Query.php
@@ -41,6 +41,8 @@ use yii\db\QueryTrait;
*
* Warning: even if you do not set any query limit, implicit LIMIT 0,20 is present by default!
*
+ * @property Connection $connection Sphinx connection instance.
+ *
* @author Paul Klimov
* @since 2.0
*/
diff --git a/extensions/sphinx/QueryBuilder.php b/extensions/sphinx/QueryBuilder.php
index e21e620..586b3a5 100644
--- a/extensions/sphinx/QueryBuilder.php
+++ b/extensions/sphinx/QueryBuilder.php
@@ -7,6 +7,7 @@
namespace yii\sphinx;
+use yii\base\InvalidParamException;
use yii\base\Object;
use yii\db\Exception;
use yii\db\Expression;
@@ -760,12 +761,12 @@ class QueryBuilder extends Object
* operator is `LIKE` or `OR LIKE` and empty if operator is `NOT LIKE` or `OR NOT LIKE`.
* @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.
+ * @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildLikeCondition($indexes, $operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
- throw new Exception("Operator '$operator' requires two operands.");
+ throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
diff --git a/extensions/sphinx/Schema.php b/extensions/sphinx/Schema.php
index 6c9571c..5515865 100644
--- a/extensions/sphinx/Schema.php
+++ b/extensions/sphinx/Schema.php
@@ -15,12 +15,12 @@ use yii\caching\GroupDependency;
/**
* Schema represents the Sphinx schema information.
*
- * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
* @property string[] $indexNames All index names in the Sphinx. This property is read-only.
- * @property string[] $indexTypes ALL index types in the Sphinx (index name => index type).
- * This property is read-only.
- * @property IndexSchema[] $tableSchemas The metadata for all indexes in the Sphinx. Each array element is an
+ * @property IndexSchema[] $indexSchemas The metadata for all indexes in the Sphinx. Each array element is an
* instance of [[IndexSchema]] or its child class. This property is read-only.
+ * @property array $indexTypes All index types in the Sphinx in format: index name => index type. This
+ * property is read-only.
+ * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only.
*
* @author Paul Klimov
* @since 2.0
diff --git a/extensions/sphinx/composer.json b/extensions/sphinx/composer.json
index decac2f..9a323d7 100644
--- a/extensions/sphinx/composer.json
+++ b/extensions/sphinx/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Asphinx",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/swiftmailer/Mailer.php b/extensions/swiftmailer/Mailer.php
index d19e013..aab8434 100644
--- a/extensions/swiftmailer/Mailer.php
+++ b/extensions/swiftmailer/Mailer.php
@@ -66,6 +66,10 @@ use yii\mail\BaseMailer;
*
* @see http://swiftmailer.org
*
+ * @property array|\Swift_Mailer $swiftMailer Swift mailer instance or array configuration. This property is
+ * read-only.
+ * @property array|\Swift_Transport $transport This property is read-only.
+ *
* @author Paul Klimov
* @since 2.0
*/
@@ -119,7 +123,7 @@ class Mailer extends BaseMailer
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function sendMessage($message)
{
diff --git a/extensions/swiftmailer/Message.php b/extensions/swiftmailer/Message.php
index b0ebd63..f7d8b34 100644
--- a/extensions/swiftmailer/Message.php
+++ b/extensions/swiftmailer/Message.php
@@ -16,7 +16,8 @@ use yii\mail\BaseMessage;
* @see Mailer
*
* @method Mailer getMailer() returns mailer instance.
- * @property \Swift_Message $swiftMessage vendor message instance.
+ *
+ * @property \Swift_Message $swiftMessage Swift message instance. This property is read-only.
*
* @author Paul Klimov
* @since 2.0
@@ -40,7 +41,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getCharset()
{
@@ -48,7 +49,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setCharset($charset)
{
@@ -57,7 +58,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getFrom()
{
@@ -65,7 +66,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setFrom($from)
{
@@ -74,7 +75,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getReplyTo()
{
@@ -82,7 +83,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setReplyTo($replyTo)
{
@@ -91,7 +92,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getTo()
{
@@ -99,7 +100,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setTo($to)
{
@@ -108,7 +109,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getCc()
{
@@ -116,7 +117,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setCc($cc)
{
@@ -125,7 +126,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getBcc()
{
@@ -133,7 +134,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setBcc($bcc)
{
@@ -142,7 +143,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function getSubject()
{
@@ -150,7 +151,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setSubject($subject)
{
@@ -159,7 +160,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setTextBody($text)
{
@@ -168,7 +169,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setHtmlBody($html)
{
@@ -221,7 +222,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attach($fileName, array $options = [])
{
@@ -237,7 +238,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function attachContent($content, array $options = [])
{
@@ -253,7 +254,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function embed($fileName, array $options = [])
{
@@ -268,7 +269,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function embedContent($content, array $options = [])
{
@@ -283,7 +284,7 @@ class Message extends BaseMessage
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function toString()
{
diff --git a/extensions/swiftmailer/composer.json b/extensions/swiftmailer/composer.json
index 5a47397..0d0953b 100644
--- a/extensions/swiftmailer/composer.json
+++ b/extensions/swiftmailer/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aswiftmailer",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/extensions/twig/composer.json b/extensions/twig/composer.json
index 8fe6431..1e7f49e 100644
--- a/extensions/twig/composer.json
+++ b/extensions/twig/composer.json
@@ -5,7 +5,7 @@
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
- "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+ "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Atwig",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
diff --git a/framework/yii/assets/jquery.maskedinput.js b/framework/yii/assets/jquery.maskedinput.js
index 49a5a72..902b5d3 100644
--- a/framework/yii/assets/jquery.maskedinput.js
+++ b/framework/yii/assets/jquery.maskedinput.js
@@ -9,7 +9,7 @@
var el = document.createElement('input'),
name = 'onpaste';
el.setAttribute(name, '');
- return (typeof el[name] === 'function')?'paste':'input';
+ return (typeof el[name] === 'function')?'paste':'input';
}
var pasteEventName = getPasteEvent() + ".mask",
@@ -322,9 +322,9 @@ $.fn.extend({
.bind("keydown.mask", keydownEvent)
.bind("keypress.mask", keypressEvent)
.bind(pasteEventName, function() {
- setTimeout(function() {
+ setTimeout(function() {
var pos=checkVal(true);
- input.caret(pos);
+ input.caret(pos);
if (settings.completed && pos == input.val().length)
settings.completed.call(input);
}, 0);
diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php
index d6409e8..4f2bee2 100644
--- a/framework/yii/base/Application.php
+++ b/framework/yii/base/Application.php
@@ -199,7 +199,7 @@ abstract class Application extends Module
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php
index c8f2d48..42a2efc 100644
--- a/framework/yii/base/Controller.php
+++ b/framework/yii/base/Controller.php
@@ -87,9 +87,9 @@ class Controller extends Component implements ViewContextInterface
*
* ~~~
* return [
- * 'action1' => '@app/components/Action1',
+ * 'action1' => 'app\components\Action1',
* 'action2' => [
- * 'class' => '@app/components/Action2',
+ * 'class' => 'app\components\Action2',
* 'property1' => 'value1',
* 'property2' => 'value2',
* ],
@@ -305,7 +305,7 @@ class Controller extends Component implements ViewContextInterface
public function render($view, $params = [])
{
$output = $this->getView()->render($view, $params, $this);
- $layoutFile = $this->findLayoutFile();
+ $layoutFile = $this->findLayoutFile($this->getView());
if ($layoutFile !== false) {
return $this->getView()->renderFile($layoutFile, ['content' => $output], $this);
} else {
@@ -386,38 +386,39 @@ class Controller extends Component implements ViewContextInterface
/**
* Finds the applicable layout file.
+ * @param View $view the view object to render the layout file
* @return string|boolean the layout file path, or false if layout is not needed.
* Please refer to [[render()]] on how to specify this parameter.
* @throws InvalidParamException if an invalid path alias is used to specify the layout
*/
- protected function findLayoutFile()
+ protected function findLayoutFile($view)
{
$module = $this->module;
if (is_string($this->layout)) {
- $view = $this->layout;
+ $layout = $this->layout;
} elseif ($this->layout === null) {
while ($module !== null && $module->layout === null) {
$module = $module->module;
}
if ($module !== null && is_string($module->layout)) {
- $view = $module->layout;
+ $layout = $module->layout;
}
}
- if (!isset($view)) {
+ if (!isset($layout)) {
return false;
}
- if (strncmp($view, '@', 1) === 0) {
- $file = Yii::getAlias($view);
- } elseif (strncmp($view, '/', 1) === 0) {
- $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
+ if (strncmp($layout, '@', 1) === 0) {
+ $file = Yii::getAlias($layout);
+ } elseif (strncmp($layout, '/', 1) === 0) {
+ $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . $layout;
} else {
- $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view;
+ $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $layout;
}
if (pathinfo($file, PATHINFO_EXTENSION) === '') {
- $file .= '.php';
+ $file .= $view->defaultExtension;
}
return $file;
}
diff --git a/framework/yii/base/ErrorException.php b/framework/yii/base/ErrorException.php
index eed6908..7c6ff6f 100644
--- a/framework/yii/base/ErrorException.php
+++ b/framework/yii/base/ErrorException.php
@@ -20,7 +20,7 @@ class ErrorException extends Exception
protected $severity;
/**
- * Constructs the exception
+ * Constructs the exception.
* @link http://php.net/manual/en/errorexception.construct.php
* @param $message [optional]
* @param $code [optional]
@@ -51,7 +51,6 @@ class ErrorException extends Exception
}
// XDebug has a different key name
- $frame['args'] = [];
if (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
}
@@ -64,17 +63,7 @@ class ErrorException extends Exception
}
/**
- * Gets the exception severity
- * @link http://php.net/manual/en/errorexception.getseverity.php
- * @return int the severity level of the exception.
- */
- final public function getSeverity()
- {
- return $this->severity;
- }
-
- /**
- * Returns if error is one of fatal type
+ * Returns if error is one of fatal type.
*
* @param array $error error got from error_get_last()
* @return bool if error is one of fatal type
@@ -85,6 +74,16 @@ class ErrorException extends Exception
}
/**
+ * Gets the exception severity.
+ * @link http://php.net/manual/en/errorexception.getseverity.php
+ * @return int the severity level of the exception.
+ */
+ final public function getSeverity()
+ {
+ return $this->severity;
+ }
+
+ /**
* @return string the user-friendly name of this exception
*/
public function getName()
diff --git a/framework/yii/base/Model.php b/framework/yii/base/Model.php
index 06f0d1f..3668826 100644
--- a/framework/yii/base/Model.php
+++ b/framework/yii/base/Model.php
@@ -249,9 +249,9 @@ class Model extends Component implements IteratorAggregate, ArrayAccess
* You may override this method to change the default behavior.
* @return array list of attribute names.
*/
- public static function attributes()
+ public function attributes()
{
- $class = new ReflectionClass(get_called_class());
+ $class = new ReflectionClass($this);
$names = [];
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
if (!$property->isStatic()) {
diff --git a/framework/yii/base/View.php b/framework/yii/base/View.php
index 3eee4ef..fcfe31e 100644
--- a/framework/yii/base/View.php
+++ b/framework/yii/base/View.php
@@ -65,6 +65,10 @@ class View extends Component
*/
public $renderers;
/**
+ * @var string the default view file extension. This will be appended to view file names if they don't have file extensions.
+ */
+ public $defaultExtension = '.php';
+ /**
* @var Theme|array the theme object or the configuration array for creating the theme object.
* If not set, it means theming is not enabled.
*/
@@ -167,7 +171,7 @@ class View extends Component
}
}
- return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file;
+ return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . $this->defaultExtension : $file;
}
/**
diff --git a/framework/yii/caching/Cache.php b/framework/yii/caching/Cache.php
index 41a25b2..371f91a 100644
--- a/framework/yii/caching/Cache.php
+++ b/framework/yii/caching/Cache.php
@@ -58,7 +58,7 @@ abstract class Cache extends Component implements \ArrayAccess
* if you don't want to use key prefix. It is recommended that you explicitly set this property to some
* static value if the cached data needs to be shared among multiple applications.
*
- * To ensure interoperability, only use alphanumeric characters should be used.
+ * To ensure interoperability, only alphanumeric characters should be used.
*/
public $keyPrefix;
/**
diff --git a/framework/yii/data/ActiveDataProvider.php b/framework/yii/data/ActiveDataProvider.php
index e9d5403..0b3fc4a 100644
--- a/framework/yii/data/ActiveDataProvider.php
+++ b/framework/yii/data/ActiveDataProvider.php
@@ -93,7 +93,7 @@ class ActiveDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareModels()
{
@@ -111,7 +111,7 @@ class ActiveDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareKeys($models)
{
@@ -150,7 +150,7 @@ class ActiveDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareTotalCount()
{
@@ -162,7 +162,7 @@ class ActiveDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function setSort($value)
{
diff --git a/framework/yii/data/ArrayDataProvider.php b/framework/yii/data/ArrayDataProvider.php
index 2b694c7..987e364 100644
--- a/framework/yii/data/ArrayDataProvider.php
+++ b/framework/yii/data/ArrayDataProvider.php
@@ -66,7 +66,7 @@ class ArrayDataProvider extends BaseDataProvider
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareModels()
{
@@ -87,7 +87,7 @@ class ArrayDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareKeys($models)
{
@@ -107,7 +107,7 @@ class ArrayDataProvider extends BaseDataProvider
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
protected function prepareTotalCount()
{
diff --git a/framework/yii/db/ActiveQuery.php b/framework/yii/db/ActiveQuery.php
index 517bf22..fb5438a 100644
--- a/framework/yii/db/ActiveQuery.php
+++ b/framework/yii/db/ActiveQuery.php
@@ -23,6 +23,7 @@ namespace yii\db;
* - [[min()]]: returns the min over the specified column.
* - [[max()]]: returns the max over the specified column.
* - [[scalar()]]: returns the value of the first column in the first row of the query result.
+ * - [[column()]]: returns the value of the first column in the query result.
* - [[exists()]]: returns a value indicating whether the query result has data or not.
*
* Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php
index ffae3d8..bb2016a 100644
--- a/framework/yii/db/ActiveRecord.php
+++ b/framework/yii/db/ActiveRecord.php
@@ -27,13 +27,13 @@ use yii\helpers\Inflector;
* @property boolean $isNewRecord Whether the record is new and should be inserted when calling [[save()]].
* @property array $oldAttributes The old attribute values (name-value pairs).
* @property mixed $oldPrimaryKey The old primary key value. An array (column name => column value) is
- * returned if the primary key is composite or `$asArray` is true. A string is returned otherwise (null will be
- * returned if the key value is null). This property is read-only.
+ * returned if the primary key is composite. A string is returned otherwise (null will be returned if the key
+ * value is null). This property is read-only.
* @property array $populatedRelations An array of relation data indexed by relation names. This property is
* read-only.
* @property mixed $primaryKey The primary key value. An array (column name => column value) is returned if
- * the primary key is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
- * the key value is null). This property is read-only.
+ * the primary key is composite. A string is returned otherwise (null will be returned if the key value is null).
+ * This property is read-only.
*
* @author Qiang Xue
* @author Carsten Brandt
@@ -316,9 +316,9 @@ class ActiveRecord extends Model
* (because another user has modified the data), a [[StaleObjectException]] exception will be thrown,
* and the update or deletion is skipped.
*
- * Optimized locking is only supported by [[update()]] and [[delete()]].
+ * Optimistic locking is only supported by [[update()]] and [[delete()]].
*
- * To use optimized locking:
+ * To use Optimistic locking:
*
* 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`.
* Override this method to return the name of this column.
@@ -469,9 +469,9 @@ class ActiveRecord extends Model
*
* @param string $class the class name of the related record
* @param array $link the primary-foreign key constraint. The keys of the array refer to
- * the columns in the table associated with the `$class` model, while the values of the
- * array refer to the corresponding columns in the table associated with this AR class.
- * @return ActiveRelation the relation object.
+ * the attributes of the record associated with the `$class` model, while the values of the
+ * array refer to the corresponding attributes in **this** AR class.
+ * @return ActiveRelationInterface the relation object.
*/
public function hasOne($class, $link)
{
@@ -508,9 +508,9 @@ class ActiveRecord extends Model
*
* @param string $class the class name of the related record
* @param array $link the primary-foreign key constraint. The keys of the array refer to
- * the columns in the table associated with the `$class` model, while the values of the
- * array refer to the corresponding columns in the table associated with this AR class.
- * @return ActiveRelation the relation object.
+ * the attributes of the record associated with the `$class` model, while the values of the
+ * array refer to the corresponding attributes in **this** AR class.
+ * @return ActiveRelationInterface the relation object.
*/
public function hasMany($class, $link)
{
@@ -570,7 +570,7 @@ class ActiveRecord extends Model
* The default implementation will return all column names of the table associated with this AR class.
* @return array list of attribute names.
*/
- public static function attributes()
+ public function attributes()
{
return array_keys(static::getTableSchema()->columns);
}
@@ -754,7 +754,7 @@ class ActiveRecord extends Model
* [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
* will be raised by the corresponding methods.
*
- * Only the [[changedAttributes|changed attribute values]] will be inserted into database.
+ * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
*
* If the table's primary key is auto-incremental and is null during insertion,
* it will be populated with the actual value after insertion.
@@ -1169,7 +1169,7 @@ class ActiveRecord extends Model
return false;
}
foreach ($this->attributes() as $name) {
- $this->_attributes[$name] = $record->_attributes[$name];
+ $this->_attributes[$name] = isset($record->_attributes[$name]) ? $record->_attributes[$name] : null;
}
$this->_oldAttributes = $this->_attributes;
$this->_related = [];
@@ -1179,11 +1179,15 @@ class ActiveRecord extends Model
/**
* Returns a value indicating whether the given active record is the same as the current one.
* The comparison is made by comparing the table names and the primary key values of the two active records.
+ * If one of the records [[isNewRecord|is new]] they are also considered not equal.
* @param ActiveRecord $record record to compare to
* @return boolean whether the two active records refer to the same row in the same database table.
*/
public function equals($record)
{
+ if ($this->isNewRecord || $record->isNewRecord) {
+ return false;
+ }
return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
}
@@ -1192,6 +1196,9 @@ class ActiveRecord extends Model
* @param boolean $asArray whether to return the primary key value as an array. If true,
* the return value will be an array with column names as keys and column values as values.
* Note that for composite primary keys, an array will always be returned regardless of this parameter value.
+ * @property mixed The primary key value. An array (column name => column value) is returned if
+ * the primary key is composite. A string is returned otherwise (null will be returned if
+ * the key value is null).
* @return mixed the primary key value. An array (column name => column value) is returned if the primary key
* is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
* the key value is null).
@@ -1218,6 +1225,9 @@ class ActiveRecord extends Model
* @param boolean $asArray whether to return the primary key value as an array. If true,
* the return value will be an array with column name as key and column value as value.
* If this is false (default), a scalar value will be returned for non-composite primary key.
+ * @property mixed The old primary key value. An array (column name => column value) is
+ * returned if the primary key is composite. A string is returned otherwise (null will be
+ * returned if the key value is null).
* @return mixed the old primary key value. An array (column name => column value) is returned if the primary key
* is composite or `$asArray` is true. A string is returned otherwise (null will be returned if
* the key value is null).
@@ -1246,7 +1256,7 @@ class ActiveRecord extends Model
public static function create($row)
{
$record = static::instantiate($row);
- $columns = array_flip(static::attributes());
+ $columns = array_flip($record->attributes());
foreach ($row as $name => $value) {
if (isset($columns[$name])) {
$record->_attributes[$name] = $value;
diff --git a/framework/yii/db/Command.php b/framework/yii/db/Command.php
index 6ed0d9c..2075b2d 100644
--- a/framework/yii/db/Command.php
+++ b/framework/yii/db/Command.php
@@ -278,9 +278,13 @@ class Command extends \yii\base\Component
return $n;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
- $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
- $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
- throw new Exception($message, $errorInfo, (int)$e->getCode(), $e);
+ if ($e instanceof Exception) {
+ throw $e;
+ } else {
+ $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
+ $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
+ throw new Exception($message, $errorInfo, (int)$e->getCode(), $e);
+ }
}
}
@@ -411,9 +415,13 @@ class Command extends \yii\base\Component
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
- $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
- $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
- throw new Exception($message, $errorInfo, (int)$e->getCode(), $e);
+ if ($e instanceof Exception) {
+ throw $e;
+ } else {
+ $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql";
+ $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
+ throw new Exception($message, $errorInfo, (int)$e->getCode(), $e);
+ }
}
}
diff --git a/framework/yii/db/Query.php b/framework/yii/db/Query.php
index 50ed105..20d13a8 100644
--- a/framework/yii/db/Query.php
+++ b/framework/yii/db/Query.php
@@ -42,7 +42,7 @@ class Query extends Component implements QueryInterface
/**
* @var array the columns being selected. For example, `['id', 'name']`.
- * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns.
+ * This is used to construct the SELECT clause in a SQL statement. If not set, it means selecting all columns.
* @see select()
*/
public $select;
diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php
index d628bc0..c4837fc 100644
--- a/framework/yii/db/QueryBuilder.php
+++ b/framework/yii/db/QueryBuilder.php
@@ -7,6 +7,7 @@
namespace yii\db;
+use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
/**
@@ -299,7 +300,7 @@ class QueryBuilder extends \yii\base\Object
{
return "DROP TABLE " . $this->db->quoteTableName($table);
}
-
+
/**
* Builds a SQL statement for adding a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint.
@@ -316,12 +317,12 @@ class QueryBuilder extends \yii\base\Object
foreach ($columns as $i => $col) {
$columns[$i] = $this->db->quoteColumnName($col);
}
-
+
return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
. $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
. implode(', ', $columns). ' )';
}
-
+
/**
* Builds a SQL statement for removing a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint to be removed.
@@ -782,7 +783,7 @@ class QueryBuilder extends \yii\base\Object
* on how to specify a condition.
* @param array $params the binding parameters to be populated
* @return string the generated SQL expression
- * @throws \yii\db\Exception if the condition is in bad format
+ * @throws InvalidParamException if the condition is in bad format
*/
public function buildCondition($condition, &$params)
{
@@ -811,7 +812,7 @@ class QueryBuilder extends \yii\base\Object
array_shift($condition);
return $this->$method($operator, $condition, $params);
} else {
- throw new Exception('Found unknown operator in query: ' . $operator);
+ throw new InvalidParamException('Found unknown operator in query: ' . $operator);
}
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params);
@@ -883,12 +884,12 @@ class QueryBuilder extends \yii\base\Object
* describe the interval that column value should be in.
* @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.
+ * @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildBetweenCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1], $operands[2])) {
- throw new Exception("Operator '$operator' requires three operands.");
+ throw new InvalidParamException("Operator '$operator' requires three operands.");
}
list($column, $value1, $value2) = $operands;
@@ -998,12 +999,12 @@ class QueryBuilder extends \yii\base\Object
* operator is `LIKE` or `OR LIKE` and empty if operator is `NOT LIKE` or `OR NOT LIKE`.
* @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.
+ * @throws InvalidParamException if wrong number of operands have been given.
*/
public function buildLikeCondition($operator, $operands, &$params)
{
if (!isset($operands[0], $operands[1])) {
- throw new Exception("Operator '$operator' requires two operands.");
+ throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $values) = $operands;
diff --git a/framework/yii/db/cubrid/QueryBuilder.php b/framework/yii/db/cubrid/QueryBuilder.php
index 7682516..45bd4a2 100644
--- a/framework/yii/db/cubrid/QueryBuilder.php
+++ b/framework/yii/db/cubrid/QueryBuilder.php
@@ -69,7 +69,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
public function buildLimit($limit, $offset)
{
@@ -83,7 +83,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
$sql .= ' OFFSET ' . (int)$offset;
}
} elseif ($offset > 0) {
- $sql = 'LIMIT ' . (int)$offset . ', 18446744073709551615'; // 2^64-1
+ $sql = 'LIMIT 9223372036854775807 OFFSET ' . (int)$offset; // 2^63-1
}
return $sql;
}
diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php
index c9bf7ca..338a74b 100644
--- a/framework/yii/db/mssql/QueryBuilder.php
+++ b/framework/yii/db/mssql/QueryBuilder.php
@@ -25,17 +25,17 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_BIGPK => 'bigint IDENTITY PRIMARY KEY',
Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'text',
- Schema::TYPE_SMALLINT => 'smallint(6)',
- Schema::TYPE_INTEGER => 'int(11)',
- Schema::TYPE_BIGINT => 'bigint(20)',
+ Schema::TYPE_SMALLINT => 'smallint',
+ Schema::TYPE_INTEGER => 'int',
+ Schema::TYPE_BIGINT => 'bigint',
Schema::TYPE_FLOAT => 'float',
- Schema::TYPE_DECIMAL => 'decimal(10,0)',
+ Schema::TYPE_DECIMAL => 'decimal',
Schema::TYPE_DATETIME => 'datetime',
Schema::TYPE_TIMESTAMP => 'timestamp',
Schema::TYPE_TIME => 'time',
Schema::TYPE_DATE => 'date',
Schema::TYPE_BINARY => 'binary',
- Schema::TYPE_BOOLEAN => 'tinyint(1)',
+ Schema::TYPE_BOOLEAN => 'bit',
Schema::TYPE_MONEY => 'decimal(19,4)',
];
diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php
index 93a06e3..9cb321f 100644
--- a/framework/yii/db/mysql/QueryBuilder.php
+++ b/framework/yii/db/mysql/QueryBuilder.php
@@ -142,7 +142,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
}
/**
- * @inheritDocs
+ * {@inheritdoc}
*/
public function buildLimit($limit, $offset)
{
diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php
index d7885f2..8cc0a39 100644
--- a/framework/yii/db/pgsql/Schema.php
+++ b/framework/yii/db/pgsql/Schema.php
@@ -196,13 +196,13 @@ EOD;
//http://www.postgresql.org/message-id/26677.1086673982@sss.pgh.pa.us
$sql = << $column) {
- $citem[$fcolumns[$idx]] = $column;
+ $citem[$column] = $fcolumns[$idx];
}
$table->foreignKeys[] = $citem;
}
@@ -241,7 +241,7 @@ SQL;
$tableName = $this->db->quoteValue($table->name);
$schemaName = $this->db->quoteValue($table->schemaName);
$sql = <<className === null ? get_class($object) : $this->className;
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
- $attributes = $className::attributes();
- if (!in_array($attributeName, $attributes)) {
- throw new InvalidConfigException("'$className' does not have an attribute named '$attributeName'.");
- }
-
$query = $className::find();
$query->where([$attributeName => $value]);
diff --git a/framework/yii/validators/Validator.php b/framework/yii/validators/Validator.php
index 2cd611b..f0602b6 100644
--- a/framework/yii/validators/Validator.php
+++ b/framework/yii/validators/Validator.php
@@ -159,7 +159,7 @@ abstract class Validator extends Component
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function init()
{
diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php
index 5c68cdb..1424d0c 100644
--- a/framework/yii/web/Controller.php
+++ b/framework/yii/web/Controller.php
@@ -79,14 +79,14 @@ class Controller extends \yii\base\Controller
'params' => implode(', ', $missing),
]));
}
-
+
$this->actionParams = $actionParams;
return $args;
}
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public function beforeAction($action)
{
diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php
index 7a7cedf..04bf0e3 100644
--- a/framework/yii/web/Request.php
+++ b/framework/yii/web/Request.php
@@ -174,7 +174,7 @@ class Request extends \yii\base\Request
{
return $this->getMethod() === 'OPTIONS';
}
-
+
/**
* Returns whether this is a HEAD request.
* @return boolean whether this is a HEAD request.
@@ -183,7 +183,7 @@ class Request extends \yii\base\Request
{
return $this->getMethod() === 'HEAD';
}
-
+
/**
* Returns whether this is a POST request.
* @return boolean whether this is a POST request.
diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php
index 3ebd358..bb77848 100644
--- a/framework/yii/web/Response.php
+++ b/framework/yii/web/Response.php
@@ -513,8 +513,8 @@ class Response extends \yii\base\Response
* specified by that header using web server internals including all optimizations like caching-headers.
*
* As this header directive is non-standard different directives exists for different web servers applications:
- *
- * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
+ *
+ * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
* - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
@@ -524,23 +524,23 @@ class Response extends \yii\base\Response
* a proper xHeader should be sent.
*
* **Note**
- *
- * This option allows to download files that are not under web folders, and even files that are otherwise protected
+ *
+ * This option allows to download files that are not under web folders, and even files that are otherwise protected
* (deny from all) like `.htaccess`.
*
* **Side effects**
- *
+ *
* If this option is disabled by the web server, when this method is called a download configuration dialog
* will open but the downloaded file will have 0 bytes.
*
* **Known issues**
- *
+ *
* There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
- * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
+ * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
* is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
*
* **Example**
- *
+ *
* ~~~
* Yii::$app->request->xSendFile('/home/user/Pictures/picture1.jpg');
* ~~~
diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php
index c3235a6..e673bae 100644
--- a/framework/yii/web/VerbFilter.php
+++ b/framework/yii/web/VerbFilter.php
@@ -51,6 +51,20 @@ class VerbFilter extends Behavior
* you add an entry with the action id as array key and an array of
* allowed methods (e.g. GET, HEAD, PUT) as the value.
* If an action is not listed all request methods are considered allowed.
+ *
+ * You can use '*' to stand for all actions. When an action is explicitly
+ * specified, it takes precedence over the specification given by '*'.
+ *
+ * For example,
+ *
+ * ~~~
+ * [
+ * 'create' => ['get', 'post'],
+ * 'update' => ['get', 'put', 'post'],
+ * 'delete' => ['post', 'delete'],
+ * '*' => ['get'],
+ * ]
+ * ~~~
*/
public $actions = [];
@@ -73,15 +87,24 @@ class VerbFilter extends Behavior
{
$action = $event->action->id;
if (isset($this->actions[$action])) {
- $verb = Yii::$app->getRequest()->getMethod();
- $allowed = array_map('strtoupper', $this->actions[$action]);
- if (!in_array($verb, $allowed)) {
- $event->isValid = false;
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
- Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed));
- throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed));
- }
+ $verbs = $this->actions[$action];
+ } elseif (isset($this->actions['*'])) {
+ $verbs = $this->actions['*'];
+ } else {
+ return $event->isValid;
}
+
+ $verb = Yii::$app->getRequest()->getMethod();
+ $allowed = array_map('strtoupper', $verbs);
+ if (!in_array($verb, array_map('strtoupper', $verbs))) {
+ $event->isValid = false;
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
+ Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed));
+ throw new HttpException(405, Yii::t('yii', 'Method Not Allowed. This url can only handle the following request methods: {methods}.', [
+ 'methods' => implode(', ', $allowed),
+ ]));
+ }
+
return $event->isValid;
}
}
diff --git a/framework/yii/widgets/DetailView.php b/framework/yii/widgets/DetailView.php
index 43d5a3e..8ced307 100644
--- a/framework/yii/widgets/DetailView.php
+++ b/framework/yii/widgets/DetailView.php
@@ -70,7 +70,7 @@ class DetailView extends Widget
* according to the "format" option.
* - format: the type of the value that determines how the value would be formatted into a displayable text.
* Please refer to [[Formatter]] for supported types.
- * - visible: whether the attribute is visible. If set to `false`, the attribute will be displayed.
+ * - visible: whether the attribute is visible. If set to `false`, the attribute will NOT be displayed.
*/
public $attributes;
/**
@@ -186,6 +186,10 @@ class DetailView extends Widget
throw new InvalidConfigException('The attribute configuration must be an array.');
}
+ if (isset($attribute['visible']) && !$attribute['visible']) {
+ continue;
+ }
+
if (!isset($attribute['format'])) {
$attribute['format'] = 'text';
}
diff --git a/tests/unit/VendorTestCase.php b/tests/unit/VendorTestCase.php
index d633d02..2002cb3 100644
--- a/tests/unit/VendorTestCase.php
+++ b/tests/unit/VendorTestCase.php
@@ -18,7 +18,7 @@ class VendorTestCase extends TestCase
*/
public static function setUpBeforeClass()
{
- $vendorDir = __DIR__ . '/vendor';
+ $vendorDir = __DIR__ . '/../../vendor';
Yii::setAlias('@vendor', $vendorDir);
$vendorAutoload = $vendorDir . '/autoload.php';
if (file_exists($vendorAutoload)) {
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index c63e002..ef17791 100644
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -8,6 +8,11 @@ define('YII_DEBUG', true);
$_SERVER['SCRIPT_NAME'] = '/' . __DIR__;
$_SERVER['SCRIPT_FILENAME'] = __FILE__;
+// require composer autoloader if available
+$composerAutoload = __DIR__ . '/../../vendor/autoload.php';
+if (is_file($composerAutoload)) {
+ require_once($composerAutoload);
+}
require_once(__DIR__ . '/../../framework/yii/Yii.php');
Yii::setAlias('@yiiunit', __DIR__);
diff --git a/tests/unit/data/ar/Customer.php b/tests/unit/data/ar/Customer.php
index 0d2add1..2d9618a 100644
--- a/tests/unit/data/ar/Customer.php
+++ b/tests/unit/data/ar/Customer.php
@@ -1,6 +1,8 @@
isNewRecord;
+ ActiveRecordTest::$afterSaveInsert = $insert;
+ ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord;
parent::afterSave($insert);
}
}
diff --git a/tests/unit/data/ar/Order.php b/tests/unit/data/ar/Order.php
index 6d5e926..476db1f 100644
--- a/tests/unit/data/ar/Order.php
+++ b/tests/unit/data/ar/Order.php
@@ -35,6 +35,22 @@ class Order extends ActiveRecord
})->orderBy('id');
}
+ public function getItemsInOrder1()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_ASC]);
+ })->orderBy('name');
+ }
+
+ public function getItemsInOrder2()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_DESC]);
+ })->orderBy('name');
+ }
+
public function getBooks()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
diff --git a/tests/unit/data/ar/elasticsearch/ActiveRecord.php b/tests/unit/data/ar/elasticsearch/ActiveRecord.php
new file mode 100644
index 0000000..aa1f304
--- /dev/null
+++ b/tests/unit/data/ar/elasticsearch/ActiveRecord.php
@@ -0,0 +1,32 @@
+
+ * @since 2.0
+ */
+class ActiveRecord extends \yii\elasticsearch\ActiveRecord
+{
+ public static $db;
+
+ /**
+ * @return \yii\elasticsearch\Connection
+ */
+ public static function getDb()
+ {
+ return self::$db;
+ }
+
+ public static function index()
+ {
+ return 'yiitest';
+ }
+}
diff --git a/tests/unit/data/ar/elasticsearch/Customer.php b/tests/unit/data/ar/elasticsearch/Customer.php
new file mode 100644
index 0000000..0c07d08
--- /dev/null
+++ b/tests/unit/data/ar/elasticsearch/Customer.php
@@ -0,0 +1,43 @@
+hasMany(Order::className(), array('customer_id' => ActiveRecord::PRIMARY_KEY_NAME))->orderBy('create_time');
+ }
+
+ public static function active($query)
+ {
+ $query->andWhere(array('status' => 1));
+ }
+
+ public function afterSave($insert)
+ {
+ ActiveRecordTest::$afterSaveInsert = $insert;
+ ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord;
+ parent::afterSave($insert);
+ }
+}
diff --git a/tests/unit/data/ar/elasticsearch/Item.php b/tests/unit/data/ar/elasticsearch/Item.php
new file mode 100644
index 0000000..7e09b0c
--- /dev/null
+++ b/tests/unit/data/ar/elasticsearch/Item.php
@@ -0,0 +1,18 @@
+hasOne(Customer::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'customer_id']);
+ }
+
+ public function getOrderItems()
+ {
+ return $this->hasMany(OrderItem::className(), ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]);
+ }
+
+ public function getItems()
+ {
+ return $this->hasMany(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id'])
+ ->via('orderItems')->orderBy('id');
+ }
+
+ public function getItemsInOrder1()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_ASC]);
+ })->orderBy('name');
+ }
+
+ public function getItemsInOrder2()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_DESC]);
+ })->orderBy('name');
+ }
+
+// public function getBooks()
+// {
+// return $this->hasMany('Item', [ActiveRecord::PRIMARY_KEY_NAME => 'item_id'])
+// ->viaTable('tbl_order_item', ['order_id' => ActiveRecord::PRIMARY_KEY_NAME])
+// ->where(['category_id' => 1]);
+// }
+
+ public function beforeSave($insert)
+ {
+ if (parent::beforeSave($insert)) {
+// $this->create_time = time();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/tests/unit/data/ar/elasticsearch/OrderItem.php b/tests/unit/data/ar/elasticsearch/OrderItem.php
new file mode 100644
index 0000000..a43d8b2
--- /dev/null
+++ b/tests/unit/data/ar/elasticsearch/OrderItem.php
@@ -0,0 +1,29 @@
+hasOne(Order::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'order_id']);
+ }
+
+ public function getItem()
+ {
+ return $this->hasOne(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']);
+ }
+}
diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php
index b48953f..63143ff 100644
--- a/tests/unit/data/ar/redis/Customer.php
+++ b/tests/unit/data/ar/redis/Customer.php
@@ -2,6 +2,8 @@
namespace yiiunit\data\ar\redis;
+use yiiunit\extensions\redis\ActiveRecordTest;
+
class Customer extends ActiveRecord
{
const STATUS_ACTIVE = 1;
@@ -9,7 +11,7 @@ class Customer extends ActiveRecord
public $status2;
- public static function attributes()
+ public function attributes()
{
return ['id', 'email', 'name', 'address', 'status'];
}
@@ -26,4 +28,11 @@ class Customer extends ActiveRecord
{
$query->andWhere(['status' => 1]);
}
+
+ public function afterSave($insert)
+ {
+ ActiveRecordTest::$afterSaveInsert = $insert;
+ ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord;
+ parent::afterSave($insert);
+ }
}
\ No newline at end of file
diff --git a/tests/unit/data/ar/redis/Item.php b/tests/unit/data/ar/redis/Item.php
index 1163265..0bcb072 100644
--- a/tests/unit/data/ar/redis/Item.php
+++ b/tests/unit/data/ar/redis/Item.php
@@ -4,7 +4,7 @@ namespace yiiunit\data\ar\redis;
class Item extends ActiveRecord
{
- public static function attributes()
+ public function attributes()
{
return ['id', 'name', 'category_id'];
}
diff --git a/tests/unit/data/ar/redis/Order.php b/tests/unit/data/ar/redis/Order.php
index 33d289a..ec5e816 100644
--- a/tests/unit/data/ar/redis/Order.php
+++ b/tests/unit/data/ar/redis/Order.php
@@ -4,7 +4,7 @@ namespace yiiunit\data\ar\redis;
class Order extends ActiveRecord
{
- public static function attributes()
+ public function attributes()
{
return ['id', 'customer_id', 'create_time', 'total'];
}
@@ -27,6 +27,22 @@ class Order extends ActiveRecord
});
}
+ public function getItemsInOrder1()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_ASC]);
+ })->orderBy('name');
+ }
+
+ public function getItemsInOrder2()
+ {
+ return $this->hasMany(Item::className(), ['id' => 'item_id'])
+ ->via('orderItems', function ($q) {
+ $q->orderBy(['subtotal' => SORT_DESC]);
+ })->orderBy('name');
+ }
+
public function getBooks()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
diff --git a/tests/unit/data/ar/redis/OrderItem.php b/tests/unit/data/ar/redis/OrderItem.php
index 38def6b..a4c82b9 100644
--- a/tests/unit/data/ar/redis/OrderItem.php
+++ b/tests/unit/data/ar/redis/OrderItem.php
@@ -2,8 +2,6 @@
namespace yiiunit\data\ar\redis;
-use yii\redis\RecordSchema;
-
class OrderItem extends ActiveRecord
{
public static function primaryKey()
@@ -11,7 +9,7 @@ class OrderItem extends ActiveRecord
return ['order_id', 'item_id'];
}
- public static function attributes()
+ public function attributes()
{
return ['order_id', 'item_id', 'quantity', 'subtotal'];
}
diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php
index e8deebb..7b23a8d 100644
--- a/tests/unit/data/config.php
+++ b/tests/unit/data/config.php
@@ -29,6 +29,9 @@ return [
'password' => 'postgres',
'fixture' => __DIR__ . '/postgres.sql',
],
+ 'elasticsearch' => [
+ 'dsn' => 'elasticsearch://localhost:9200'
+ ],
'redis' => [
'hostname' => 'localhost',
'port' => 6379,
@@ -39,7 +42,7 @@ return [
'sphinx' => [
'sphinx' => [
'dsn' => 'mysql:host=127.0.0.1;port=9306;',
- 'username' => '',
+ 'username' => 'travis',
'password' => '',
],
'db' => [
diff --git a/tests/unit/data/cubrid.sql b/tests/unit/data/cubrid.sql
index 905ebd2..1fe75ed 100644
--- a/tests/unit/data/cubrid.sql
+++ b/tests/unit/data/cubrid.sql
@@ -23,7 +23,7 @@ CREATE TABLE `tbl_constraints`
CREATE TABLE `tbl_customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(128) NOT NULL,
- `name` varchar(128) NOT NULL,
+ `name` varchar(128),
`address` string,
`status` int (11) DEFAULT 0,
PRIMARY KEY (`id`)
diff --git a/tests/unit/data/mssql.sql b/tests/unit/data/mssql.sql
index 2c29fa4..a074205 100644
--- a/tests/unit/data/mssql.sql
+++ b/tests/unit/data/mssql.sql
@@ -9,7 +9,7 @@ IF OBJECT_ID('[dbo].[tbl_null_values]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_n
CREATE TABLE [dbo].[tbl_customer] (
[id] [int] IDENTITY(1,1) NOT NULL,
[email] [varchar](128) NOT NULL,
- [name] [varchar](128) NOT NULL,
+ [name] [varchar](128),
[address] [text],
[status] [int] DEFAULT 0,
CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED (
diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql
index 43322ad..ff5b72e 100644
--- a/tests/unit/data/mysql.sql
+++ b/tests/unit/data/mysql.sql
@@ -23,7 +23,7 @@ CREATE TABLE `tbl_constraints`
CREATE TABLE `tbl_customer` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(128) NOT NULL,
- `name` varchar(128) NOT NULL,
+ `name` varchar(128),
`address` text,
`status` int (11) DEFAULT 0,
PRIMARY KEY (`id`)
diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql
index f9ee192..8d5cb4f 100644
--- a/tests/unit/data/postgres.sql
+++ b/tests/unit/data/postgres.sql
@@ -22,7 +22,7 @@ CREATE TABLE tbl_constraints
CREATE TABLE tbl_customer (
id serial not null primary key,
email varchar(128) NOT NULL,
- name varchar(128) NOT NULL,
+ name varchar(128),
address text,
status integer DEFAULT 0
);
diff --git a/tests/unit/data/sphinx/ar/ArticleIndex.php b/tests/unit/data/sphinx/ar/ArticleIndex.php
index 767fdea..757f6be 100644
--- a/tests/unit/data/sphinx/ar/ArticleIndex.php
+++ b/tests/unit/data/sphinx/ar/ArticleIndex.php
@@ -20,12 +20,12 @@ class ArticleIndex extends ActiveRecord
public function getSource()
{
- return $this->hasOne('db', ArticleDb::className(), ['id' => 'id']);
+ return $this->hasOne(ArticleDb::className(), ['id' => 'id']);
}
public function getTags()
{
- return $this->hasMany('db', TagDb::className(), ['id' => 'tag']);
+ return $this->hasMany(TagDb::className(), ['id' => 'tag']);
}
public function getSnippetSource()
diff --git a/tests/unit/data/sphinx/sphinx.conf b/tests/unit/data/sphinx/sphinx.conf
index 30cf083..5672e06 100644
--- a/tests/unit/data/sphinx/sphinx.conf
+++ b/tests/unit/data/sphinx/sphinx.conf
@@ -2,7 +2,7 @@
#
# Setup test environment:
# - initialize test database source:
-# mysql -D yii2test -u test < /path/to/yii/tests/unit/data/sphinx/source.sql
+# mysql -D yiitest -u test < /path/to/yii/tests/unit/data/sphinx/source.sql
# - setup test Sphinx indexes:
# indexer --config /path/to/yii/tests/unit/data/sphinx/sphinx.conf --all [--rotate]
# - run the "searchd" daemon:
@@ -14,9 +14,9 @@ source yii2_test_article_src
type = mysql
sql_host = localhost
- sql_user =
+ sql_user = travis
sql_pass =
- sql_db = yii2test
+ sql_db = yiitest
sql_port = 3306 # optional, default is 3306
sql_query = \
@@ -37,9 +37,9 @@ source yii2_test_item_src
type = mysql
sql_host = localhost
- sql_user =
+ sql_user = travis
sql_pass =
- sql_db = yii2test
+ sql_db = yiitest
sql_port = 3306 # optional, default is 3306
sql_query = \
diff --git a/tests/unit/data/sqlite.sql b/tests/unit/data/sqlite.sql
index ff79c66..ba8a208 100644
--- a/tests/unit/data/sqlite.sql
+++ b/tests/unit/data/sqlite.sql
@@ -15,7 +15,7 @@ DROP TABLE IF EXISTS tbl_null_values;
CREATE TABLE tbl_customer (
id INTEGER NOT NULL,
email varchar(128) NOT NULL,
- name varchar(128) NOT NULL,
+ name varchar(128),
address text,
status INTEGER DEFAULT 0,
PRIMARY KEY (id)
diff --git a/tests/unit/data/travis/README.md b/tests/unit/data/travis/README.md
index c86497e..ad9538d 100644
--- a/tests/unit/data/travis/README.md
+++ b/tests/unit/data/travis/README.md
@@ -10,3 +10,5 @@ The scripts are:
Compiles and installs the [memcache pecl extension](http://pecl.php.net/package/memcache)
- [`cubrid-setup.sh`](cubrid-setup.sh)
Prepares the [CUBRID](http://www.cubrid.org/) server instance by installing the server and PHP PDO driver
+ - [`sphinx-setup.sh`](sphinx-setup.sh)
+ Prepares the [Sphinx](http://sphinxsearch.com/) server instances by installing the server and attaching it to MySQL
\ No newline at end of file
diff --git a/tests/unit/data/travis/sphinx-setup.sh b/tests/unit/data/travis/sphinx-setup.sh
new file mode 100755
index 0000000..f0ec75e
--- /dev/null
+++ b/tests/unit/data/travis/sphinx-setup.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+SCRIPT=$(readlink -f "$0")
+CWD=$(dirname "$SCRIPT")
+
+# install sphinxsearch:
+echo 'yes' | sudo add-apt-repository ppa:builds/sphinxsearch-daily
+sudo apt-get update
+sudo apt-get install sphinxsearch
+
+# log files
+sudo mkdir /var/log/sphinx
+sudo touch /var/log/sphinx/searchd.log
+sudo touch /var/log/sphinx/query.log
+sudo chmod -R 777 /var/log/sphinx # ugly (for travis)
+
+# spl dir
+sudo mkdir /var/lib/sphinx
+sudo chmod 777 /var/lib/sphinx # ugly (for travis)
+
+# run dir pid
+sudo mkdir /var/run/sphinx
+sudo chmod 777 /var/run/sphinx # ugly (for travis)
+
+# setup test Sphinx indexes:
+indexer --config $CWD/../sphinx/sphinx.conf --all
+
+# run searchd:
+searchd --config $CWD/../sphinx/sphinx.conf
diff --git a/tests/unit/extensions/elasticsearch/ActiveRecordTest.php b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php
new file mode 100644
index 0000000..ccdc90f
--- /dev/null
+++ b/tests/unit/extensions/elasticsearch/ActiveRecordTest.php
@@ -0,0 +1,495 @@
+getConnection()->createCommand()->flushIndex('yiitest');
+ }
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ /** @var Connection $db */
+ $db = ActiveRecord::$db = $this->getConnection();
+
+ // delete index
+ if ($db->createCommand()->indexExists('yiitest')) {
+ $db->createCommand()->deleteIndex('yiitest');
+ }
+
+ $db->post(['yiitest'], [], Json::encode([
+ 'mappings' => [
+ "item" => [
+ "_source" => [ "enabled" => true ],
+ "properties" => [
+ // allow proper sorting by name
+ "name" => ["type" => "string", "index" => "not_analyzed"],
+ ]
+ ]
+ ],
+ ]));
+
+ $customer = new Customer();
+ $customer->id = 1;
+ $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false);
+ $customer->save(false);
+ $customer = new Customer();
+ $customer->id = 2;
+ $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false);
+ $customer->save(false);
+ $customer = new Customer();
+ $customer->id = 3;
+ $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false);
+ $customer->save(false);
+
+// INSERT INTO tbl_category (name) VALUES ('Books');
+// INSERT INTO tbl_category (name) VALUES ('Movies');
+
+ $item = new Item();
+ $item->id = 1;
+ $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false);
+ $item->save(false);
+ $item = new Item();
+ $item->id = 2;
+ $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false);
+ $item->save(false);
+ $item = new Item();
+ $item->id = 3;
+ $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false);
+ $item->save(false);
+ $item = new Item();
+ $item->id = 4;
+ $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false);
+ $item->save(false);
+ $item = new Item();
+ $item->id = 5;
+ $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false);
+ $item->save(false);
+
+ $order = new Order();
+ $order->id = 1;
+ $order->setAttributes(['customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0], false);
+ $order->save(false);
+ $order = new Order();
+ $order->id = 2;
+ $order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false);
+ $order->save(false);
+ $order = new Order();
+ $order->id = 3;
+ $order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], false);
+ $order->save(false);
+
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false);
+ $orderItem->save(false);
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false);
+ $orderItem->save(false);
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false);
+ $orderItem->save(false);
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false);
+ $orderItem->save(false);
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false);
+ $orderItem->save(false);
+ $orderItem = new OrderItem();
+ $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false);
+ $orderItem->save(false);
+
+ $db->createCommand()->flushIndex('yiitest');
+ }
+
+ public function testSearch()
+ {
+ $customers = $this->callCustomerFind()->search()['hits'];
+ $this->assertEquals(3, $customers['total']);
+ $this->assertEquals(3, count($customers['hits']));
+ $this->assertTrue($customers['hits'][0] instanceof Customer);
+ $this->assertTrue($customers['hits'][1] instanceof Customer);
+ $this->assertTrue($customers['hits'][2] instanceof Customer);
+
+ // limit vs. totalcount
+ $customers = $this->callCustomerFind()->limit(2)->search()['hits'];
+ $this->assertEquals(3, $customers['total']);
+ $this->assertEquals(2, count($customers['hits']));
+
+ // asArray
+ $result = $this->callCustomerFind()->asArray()->search()['hits'];
+ $this->assertEquals(3, $result['total']);
+ $customers = $result['hits'];
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers[0]);
+ $this->assertArrayHasKey('name', $customers[0]);
+ $this->assertArrayHasKey('email', $customers[0]);
+ $this->assertArrayHasKey('address', $customers[0]);
+ $this->assertArrayHasKey('status', $customers[0]);
+ $this->assertArrayHasKey('id', $customers[1]);
+ $this->assertArrayHasKey('name', $customers[1]);
+ $this->assertArrayHasKey('email', $customers[1]);
+ $this->assertArrayHasKey('address', $customers[1]);
+ $this->assertArrayHasKey('status', $customers[1]);
+ $this->assertArrayHasKey('id', $customers[2]);
+ $this->assertArrayHasKey('name', $customers[2]);
+ $this->assertArrayHasKey('email', $customers[2]);
+ $this->assertArrayHasKey('address', $customers[2]);
+ $this->assertArrayHasKey('status', $customers[2]);
+
+ // TODO test asArray() + fields() + indexBy()
+
+ // find by attributes
+ $result = $this->callCustomerFind()->where(['name' => 'user2'])->search()['hits'];
+ $customer = reset($result['hits']);
+ $this->assertTrue($customer instanceof Customer);
+ $this->assertEquals(2, $customer->id);
+
+ // TODO test query() and filter()
+ }
+
+ public function testSearchFacets()
+ {
+ $result = $this->callCustomerFind()->addStatisticalFacet('status_stats', ['field' => 'status'])->search();
+ $this->assertArrayHasKey('facets', $result);
+ $this->assertEquals(3, $result['facets']['status_stats']['count']);
+ $this->assertEquals(4, $result['facets']['status_stats']['total']); // sum of values
+ $this->assertEquals(1, $result['facets']['status_stats']['min']);
+ $this->assertEquals(2, $result['facets']['status_stats']['max']);
+ }
+
+ public function testGetDb()
+ {
+ $this->mockApplication(['components' => ['elasticsearch' => Connection::className()]]);
+ $this->assertInstanceOf(Connection::className(), ActiveRecord::getDb());
+ }
+
+ public function testGet()
+ {
+ $this->assertInstanceOf(Customer::className(), Customer::get(1));
+ $this->assertNull(Customer::get(5));
+ }
+
+ public function testMget()
+ {
+ $this->assertEquals([], Customer::mget([]));
+
+ $records = Customer::mget([1]);
+ $this->assertEquals(1, count($records));
+ $this->assertInstanceOf(Customer::className(), reset($records));
+
+ $records = Customer::mget([5]);
+ $this->assertEquals(0, count($records));
+
+ $records = Customer::mget([1,3,5]);
+ $this->assertEquals(2, count($records));
+ $this->assertInstanceOf(Customer::className(), $records[0]);
+ $this->assertInstanceOf(Customer::className(), $records[1]);
+ }
+
+ public function testFindLazy()
+ {
+ /** @var $customer Customer */
+ $customer = Customer::find(2);
+ $orders = $customer->orders;
+ $this->assertEquals(2, count($orders));
+
+ $orders = $customer->getOrders()->where(['between', 'create_time', 1325334000, 1325400000])->all();
+ $this->assertEquals(1, count($orders));
+ $this->assertEquals(2, $orders[0]->id);
+ }
+
+ public function testFindEagerViaRelation()
+ {
+ $orders = Order::find()->with('items')->orderBy('create_time')->all();
+ $this->assertEquals(3, count($orders));
+ $order = $orders[0];
+ $this->assertEquals(1, $order->id);
+ $this->assertTrue($order->isRelationPopulated('items'));
+ $this->assertEquals(2, count($order->items));
+ $this->assertEquals(1, $order->items[0]->id);
+ $this->assertEquals(2, $order->items[1]->id);
+ }
+
+ public function testInsertNoPk()
+ {
+ $this->assertEquals([ActiveRecord::PRIMARY_KEY_NAME], Customer::primaryKey());
+ $pkName = ActiveRecord::PRIMARY_KEY_NAME;
+
+ $customer = new Customer;
+ $customer->email = 'user4@example.com';
+ $customer->name = 'user4';
+ $customer->address = 'address4';
+
+ $this->assertNull($customer->primaryKey);
+ $this->assertNull($customer->oldPrimaryKey);
+ $this->assertNull($customer->$pkName);
+ $this->assertTrue($customer->isNewRecord);
+
+ $customer->save();
+
+ $this->assertNotNull($customer->primaryKey);
+ $this->assertNotNull($customer->oldPrimaryKey);
+ $this->assertNotNull($customer->$pkName);
+ $this->assertEquals($customer->primaryKey, $customer->oldPrimaryKey);
+ $this->assertEquals($customer->primaryKey, $customer->$pkName);
+ $this->assertFalse($customer->isNewRecord);
+ }
+
+ public function testInsertPk()
+ {
+ $pkName = ActiveRecord::PRIMARY_KEY_NAME;
+
+ $customer = new Customer;
+ $customer->$pkName = 5;
+ $customer->email = 'user5@example.com';
+ $customer->name = 'user5';
+ $customer->address = 'address5';
+
+ $this->assertTrue($customer->isNewRecord);
+
+ $customer->save();
+
+ $this->assertEquals(5, $customer->primaryKey);
+ $this->assertEquals(5, $customer->oldPrimaryKey);
+ $this->assertEquals(5, $customer->$pkName);
+ $this->assertFalse($customer->isNewRecord);
+ }
+
+ public function testUpdatePk()
+ {
+ $pkName = ActiveRecord::PRIMARY_KEY_NAME;
+
+ $pk = [$pkName => 2];
+ $orderItem = Order::find($pk);
+ $this->assertEquals(2, $orderItem->primaryKey);
+ $this->assertEquals(2, $orderItem->oldPrimaryKey);
+ $this->assertEquals(2, $orderItem->$pkName);
+
+ $this->setExpectedException('yii\base\InvalidCallException');
+ $orderItem->$pkName = 13;
+ $orderItem->save();
+ }
+
+ public function testFindLazyVia2()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ /** @var Order $order */
+ $orderClass = $this->getOrderClass();
+ $pkName = ActiveRecord::PRIMARY_KEY_NAME;
+
+ $order = new $orderClass();
+ $order->$pkName = 100;
+ $this->assertEquals([], $order->items);
+ }
+
+ public function testUpdateCounters()
+ {
+ // Update Counters is not supported by elasticsearch
+// $this->setExpectedException('yii\base\NotSupportedException');
+// ActiveRecordTestTrait::testUpdateCounters();
+ }
+
+ /**
+ * Some PDO implementations(e.g. cubrid) do not support boolean values.
+ * Make sure this does not affect AR layer.
+ */
+ public function testBooleanAttribute()
+ {
+ $db = $this->getConnection();
+ $db->createCommand()->deleteIndex('yiitest');
+ $db->post(['yiitest'], [], Json::encode([
+ 'mappings' => [
+ "customer" => [
+ "_source" => [ "enabled" => true ],
+ "properties" => [
+ // this is for the boolean test
+ "status" => ["type" => "boolean"],
+ ]
+ ]
+ ],
+ ]));
+
+ $customerClass = $this->getCustomerClass();
+ $customer = new $customerClass();
+ $customer->name = 'boolean customer';
+ $customer->email = 'mail@example.com';
+ $customer->status = true;
+ $customer->save(false);
+
+ $customer->refresh();
+ $this->assertEquals(true, $customer->status);
+
+ $customer->status = false;
+ $customer->save(false);
+
+ $customer->refresh();
+ $this->assertEquals(false, $customer->status);
+
+ $customer = new Customer();
+ $customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false);
+ $customer->save(false);
+ $customer = new Customer();
+ $customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false);
+ $customer->save(false);
+ $this->afterSave();
+
+ $customers = $this->callCustomerFind()->where(['status' => true])->all();
+ $this->assertEquals(1, count($customers));
+
+ $customers = $this->callCustomerFind()->where(['status' => false])->all();
+ $this->assertEquals(2, count($customers));
+ }
+
+ public function testfindAsArrayFields()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // indexBy + asArray
+ $customers = $this->callCustomerFind()->asArray()->fields(['id', 'name'])->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers[0]);
+ $this->assertArrayHasKey('name', $customers[0]);
+ $this->assertArrayNotHasKey('email', $customers[0]);
+ $this->assertArrayNotHasKey('address', $customers[0]);
+ $this->assertArrayNotHasKey('status', $customers[0]);
+ $this->assertArrayHasKey('id', $customers[1]);
+ $this->assertArrayHasKey('name', $customers[1]);
+ $this->assertArrayNotHasKey('email', $customers[1]);
+ $this->assertArrayNotHasKey('address', $customers[1]);
+ $this->assertArrayNotHasKey('status', $customers[1]);
+ $this->assertArrayHasKey('id', $customers[2]);
+ $this->assertArrayHasKey('name', $customers[2]);
+ $this->assertArrayNotHasKey('email', $customers[2]);
+ $this->assertArrayNotHasKey('address', $customers[2]);
+ $this->assertArrayNotHasKey('status', $customers[2]);
+ }
+
+ public function testfindIndexByFields()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // indexBy + asArray
+ $customers = $this->callCustomerFind()->indexBy('name')->fields('id', 'name')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers['user1'] instanceof $customerClass);
+ $this->assertTrue($customers['user2'] instanceof $customerClass);
+ $this->assertTrue($customers['user3'] instanceof $customerClass);
+ $this->assertNotNull($customers['user1']->id);
+ $this->assertNotNull($customers['user1']->name);
+ $this->assertNull($customers['user1']->email);
+ $this->assertNull($customers['user1']->address);
+ $this->assertNull($customers['user1']->status);
+ $this->assertNotNull($customers['user2']->id);
+ $this->assertNotNull($customers['user2']->name);
+ $this->assertNull($customers['user2']->email);
+ $this->assertNull($customers['user2']->address);
+ $this->assertNull($customers['user2']->status);
+ $this->assertNotNull($customers['user3']->id);
+ $this->assertNotNull($customers['user3']->name);
+ $this->assertNull($customers['user3']->email);
+ $this->assertNull($customers['user3']->address);
+ $this->assertNull($customers['user3']->status);
+
+ // indexBy callable + asArray
+ $customers = $this->callCustomerFind()->indexBy(function ($customer) {
+ return $customer->id . '-' . $customer->name;
+ })->fields('id', 'name')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers['1-user1'] instanceof $customerClass);
+ $this->assertTrue($customers['2-user2'] instanceof $customerClass);
+ $this->assertTrue($customers['3-user3'] instanceof $customerClass);
+ $this->assertNotNull($customers['1-user1']->id);
+ $this->assertNotNull($customers['1-user1']->name);
+ $this->assertNull($customers['1-user1']->email);
+ $this->assertNull($customers['1-user1']->address);
+ $this->assertNull($customers['1-user1']->status);
+ $this->assertNotNull($customers['2-user2']->id);
+ $this->assertNotNull($customers['2-user2']->name);
+ $this->assertNull($customers['2-user2']->email);
+ $this->assertNull($customers['2-user2']->address);
+ $this->assertNull($customers['2-user2']->status);
+ $this->assertNotNull($customers['3-user3']->id);
+ $this->assertNotNull($customers['3-user3']->name);
+ $this->assertNull($customers['3-user3']->email);
+ $this->assertNull($customers['3-user3']->address);
+ $this->assertNull($customers['3-user3']->status);
+ }
+
+ public function testfindIndexByAsArrayFields()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // indexBy + asArray
+ $customers = $this->callCustomerFind()->indexBy('name')->asArray()->fields('id', 'name')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers['user1']);
+ $this->assertArrayHasKey('name', $customers['user1']);
+ $this->assertArrayNotHasKey('email', $customers['user1']);
+ $this->assertArrayNotHasKey('address', $customers['user1']);
+ $this->assertArrayNotHasKey('status', $customers['user1']);
+ $this->assertArrayHasKey('id', $customers['user2']);
+ $this->assertArrayHasKey('name', $customers['user2']);
+ $this->assertArrayNotHasKey('email', $customers['user2']);
+ $this->assertArrayNotHasKey('address', $customers['user2']);
+ $this->assertArrayNotHasKey('status', $customers['user2']);
+ $this->assertArrayHasKey('id', $customers['user3']);
+ $this->assertArrayHasKey('name', $customers['user3']);
+ $this->assertArrayNotHasKey('email', $customers['user3']);
+ $this->assertArrayNotHasKey('address', $customers['user3']);
+ $this->assertArrayNotHasKey('status', $customers['user3']);
+
+ // indexBy callable + asArray
+ $customers = $this->callCustomerFind()->indexBy(function ($customer) {
+ return $customer['id'] . '-' . $customer['name'];
+ })->asArray()->fields('id', 'name')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers['1-user1']);
+ $this->assertArrayHasKey('name', $customers['1-user1']);
+ $this->assertArrayNotHasKey('email', $customers['1-user1']);
+ $this->assertArrayNotHasKey('address', $customers['1-user1']);
+ $this->assertArrayNotHasKey('status', $customers['1-user1']);
+ $this->assertArrayHasKey('id', $customers['2-user2']);
+ $this->assertArrayHasKey('name', $customers['2-user2']);
+ $this->assertArrayNotHasKey('email', $customers['2-user2']);
+ $this->assertArrayNotHasKey('address', $customers['2-user2']);
+ $this->assertArrayNotHasKey('status', $customers['2-user2']);
+ $this->assertArrayHasKey('id', $customers['3-user3']);
+ $this->assertArrayHasKey('name', $customers['3-user3']);
+ $this->assertArrayNotHasKey('email', $customers['3-user3']);
+ $this->assertArrayNotHasKey('address', $customers['3-user3']);
+ $this->assertArrayNotHasKey('status', $customers['3-user3']);
+ }
+
+
+}
\ No newline at end of file
diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php
new file mode 100644
index 0000000..60b2428
--- /dev/null
+++ b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php
@@ -0,0 +1,28 @@
+autodetectCluster;
+ $connection->nodes = [
+ ['http_address' => 'inet[/127.0.0.1:9200]'],
+ ];
+ $this->assertNull($connection->activeNode);
+ $connection->open();
+ $this->assertNotNull($connection->activeNode);
+ $this->assertArrayHasKey('name', reset($connection->nodes));
+ $this->assertArrayHasKey('hostname', reset($connection->nodes));
+ $this->assertArrayHasKey('version', reset($connection->nodes));
+ $this->assertArrayHasKey('http_address', reset($connection->nodes));
+ }
+
+}
\ No newline at end of file
diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php b/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php
new file mode 100644
index 0000000..e0435a7
--- /dev/null
+++ b/tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php
@@ -0,0 +1,51 @@
+mockApplication();
+
+ $databases = $this->getParam('databases');
+ $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null;
+ if ($params === null || !isset($params['dsn'])) {
+ $this->markTestSkipped('No elasticsearch server connection configured.');
+ }
+ $dsn = explode('/', $params['dsn']);
+ $host = $dsn[2];
+ if (strpos($host, ':')===false) {
+ $host .= ':9200';
+ }
+ if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) {
+ $this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription);
+ }
+
+ parent::setUp();
+ }
+
+ /**
+ * @param bool $reset whether to clean up the test database
+ * @return Connection
+ */
+ public function getConnection($reset = true)
+ {
+ $databases = $this->getParam('databases');
+ $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array();
+ $db = new Connection();
+ if ($reset) {
+ $db->open();
+ }
+ return $db;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/extensions/elasticsearch/QueryTest.php b/tests/unit/extensions/elasticsearch/QueryTest.php
new file mode 100644
index 0000000..da2558e
--- /dev/null
+++ b/tests/unit/extensions/elasticsearch/QueryTest.php
@@ -0,0 +1,185 @@
+getConnection()->createCommand();
+
+ $command->deleteAllIndexes();
+
+ $command->insert('test', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1);
+ $command->insert('test', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2);
+ $command->insert('test', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3);
+ $command->insert('test', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4);
+
+ $command->flushIndex();
+ }
+
+ public function testFields()
+ {
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $query->fields(['name', 'status']);
+ $this->assertEquals(['name', 'status'], $query->fields);
+
+ $query->fields('name', 'status');
+ $this->assertEquals(['name', 'status'], $query->fields);
+
+ $result = $query->one($this->getConnection());
+ $this->assertEquals(2, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+
+ $query->fields([]);
+ $this->assertEquals([], $query->fields);
+
+ $result = $query->one($this->getConnection());
+ $this->assertEquals([], $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+
+ $query->fields(null);
+ $this->assertNull($query->fields);
+
+ $result = $query->one($this->getConnection());
+ $this->assertEquals(3, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('email', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+ }
+
+ public function testOne()
+ {
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $result = $query->one($this->getConnection());
+ $this->assertEquals(3, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('email', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+
+ $result = $query->where(['name' => 'user1'])->one($this->getConnection());
+ $this->assertEquals(3, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('email', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+ $this->assertEquals(1, $result['_id']);
+
+ $result = $query->where(['name' => 'user5'])->one($this->getConnection());
+ $this->assertFalse($result);
+ }
+
+ public function testAll()
+ {
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $results = $query->all($this->getConnection());
+ $this->assertEquals(4, count($results));
+ $result = reset($results);
+ $this->assertEquals(3, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('email', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $results = $query->where(['name' => 'user1'])->all($this->getConnection());
+ $this->assertEquals(1, count($results));
+ $result = reset($results);
+ $this->assertEquals(3, count($result['_source']));
+ $this->assertArrayHasKey('status', $result['_source']);
+ $this->assertArrayHasKey('email', $result['_source']);
+ $this->assertArrayHasKey('name', $result['_source']);
+ $this->assertArrayHasKey('_id', $result);
+ $this->assertEquals(1, $result['_id']);
+
+ // indexBy
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $results = $query->indexBy('name')->all($this->getConnection());
+ $this->assertEquals(4, count($results));
+ ksort($results);
+ $this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results));
+ }
+
+ public function testScalar()
+ {
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection());
+ $this->assertEquals('user1', $result);
+ $result = $query->where(['name' => 'user1'])->scalar('noname', $this->getConnection());
+ $this->assertNull($result);
+ $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection());
+ $this->assertNull($result);
+ }
+
+ public function testColumn()
+ {
+ $query = new Query;
+ $query->from('test', 'user');
+
+ $result = $query->orderBy(['name' => SORT_ASC])->column('name', $this->getConnection());
+ $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result);
+ $result = $query->column('noname', $this->getConnection());
+ $this->assertEquals([null, null, null, null], $result);
+ $result = $query->where(['name' => 'user5'])->scalar('name', $this->getConnection());
+ $this->assertNull($result);
+
+ }
+
+ // TODO test facets
+
+ // TODO test complex where() every edge of QueryBuilder
+
+ public function testOrder()
+ {
+ $query = new Query;
+ $query->orderBy('team');
+ $this->assertEquals(['team' => SORT_ASC], $query->orderBy);
+
+ $query->addOrderBy('company');
+ $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy);
+
+ $query->addOrderBy('age');
+ $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy);
+
+ $query->addOrderBy(['age' => SORT_DESC]);
+ $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy);
+
+ $query->addOrderBy('age ASC, company DESC');
+ $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy);
+ }
+
+ public function testLimitOffset()
+ {
+ $query = new Query;
+ $query->limit(10)->offset(5);
+ $this->assertEquals(10, $query->limit);
+ $this->assertEquals(5, $query->offset);
+ }
+
+ public function testUnion()
+ {
+ }
+}
diff --git a/tests/unit/extensions/redis/ActiveRecordTest.php b/tests/unit/extensions/redis/ActiveRecordTest.php
index 74dd49e..f3cbbdc 100644
--- a/tests/unit/extensions/redis/ActiveRecordTest.php
+++ b/tests/unit/extensions/redis/ActiveRecordTest.php
@@ -8,12 +8,26 @@ use yiiunit\data\ar\redis\Customer;
use yiiunit\data\ar\redis\OrderItem;
use yiiunit\data\ar\redis\Order;
use yiiunit\data\ar\redis\Item;
+use yiiunit\framework\ar\ActiveRecordTestTrait;
/**
* @group redis
*/
class ActiveRecordTest extends RedisTestCase
{
+ use ActiveRecordTestTrait;
+
+ public function callCustomerFind($q = null) { return Customer::find($q); }
+ public function callOrderFind($q = null) { return Order::find($q); }
+ public function callOrderItemFind($q = null) { return OrderItem::find($q); }
+ public function callItemFind($q = null) { return Item::find($q); }
+
+ public function getCustomerClass() { return Customer::className(); }
+ public function getItemClass() { return Item::className(); }
+ public function getOrderClass() { return Order::className(); }
+ public function getOrderItemClass() { return OrderItem::className(); }
+
+
public function setUp()
{
parent::setUp();
@@ -78,50 +92,30 @@ class ActiveRecordTest extends RedisTestCase
$orderItem->save(false);
}
- public function testFind()
+ public function testFindNullValues()
{
- // find one
- $result = Customer::find();
- $this->assertTrue($result instanceof ActiveQuery);
- $customer = $result->one();
- $this->assertTrue($customer instanceof Customer);
-
- // find all
- $customers = Customer::find()->all();
- $this->assertEquals(3, count($customers));
- $this->assertTrue($customers[0] instanceof Customer);
- $this->assertTrue($customers[1] instanceof Customer);
- $this->assertTrue($customers[2] instanceof Customer);
+ // https://github.com/yiisoft/yii2/issues/1311
+ $this->markTestSkipped('Redis does not store/find null values correctly.');
+ }
- // find by a single primary key
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer = Customer::find(5);
- $this->assertNull($customer);
- $customer = Customer::find(['id' => [5, 6, 1]]);
- $this->assertEquals(1, count($customer));
- $customer = Customer::find()->where(['id' => [5, 6, 1]])->one();
- $this->assertNotNull($customer);
-
- // query scalar
- $customerName = Customer::find()->where(['id' => 2])->scalar('name');
- $this->assertEquals('user2', $customerName);
-
- // find by column values
- $customer = Customer::find(['id' => 2, 'name' => 'user2']);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer = Customer::find(['id' => 2, 'name' => 'user1']);
- $this->assertNull($customer);
- $customer = Customer::find(['id' => 5]);
- $this->assertNull($customer);
+ public function testBooleanAttribute()
+ {
+ // https://github.com/yiisoft/yii2/issues/1311
+ $this->markTestSkipped('Redis does not store/find boolean values correctly.');
+ }
+
+ public function testFindEagerViaRelationPreserveOrder()
+ {
+ $this->markTestSkipped('Redis does not support orderBy.');
+ }
- // find by attributes
- $customer = Customer::find()->where(['name' => 'user2'])->one();
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals(2, $customer->id);
+ public function testFindEagerViaRelationPreserveOrderB()
+ {
+ $this->markTestSkipped('Redis does not support orderBy.');
+ }
+ public function testSatisticalFind()
+ {
// find count, sum, average, min, max, scalar
$this->assertEquals(3, Customer::find()->count());
$this->assertEquals(6, Customer::find()->sum('id'));
@@ -129,156 +123,80 @@ class ActiveRecordTest extends RedisTestCase
$this->assertEquals(1, Customer::find()->min('id'));
$this->assertEquals(3, Customer::find()->max('id'));
- // scope
- $this->assertEquals(2, Customer::find()->active()->count());
-
- // asArray
- $customer = Customer::find()->where(['id' => 2])->asArray()->one();
- $this->assertEquals(array(
- 'id' => '2',
- 'email' => 'user2@example.com',
- 'name' => 'user2',
- 'address' => 'address2',
- 'status' => '1',
- ), $customer);
+ $this->assertEquals(6, OrderItem::find()->count());
+ $this->assertEquals(7, OrderItem::find()->sum('quantity'));
+ }
+ public function testfindIndexBy()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
// indexBy
- $customers = Customer::find()->indexBy('name')->all();
+ $customers = $this->callCustomerFind()->indexBy('name')/*->orderBy('id')*/->all();
$this->assertEquals(3, count($customers));
- $this->assertTrue($customers['user1'] instanceof Customer);
- $this->assertTrue($customers['user2'] instanceof Customer);
- $this->assertTrue($customers['user3'] instanceof Customer);
+ $this->assertTrue($customers['user1'] instanceof $customerClass);
+ $this->assertTrue($customers['user2'] instanceof $customerClass);
+ $this->assertTrue($customers['user3'] instanceof $customerClass);
// indexBy callable
- $customers = Customer::find()->indexBy(function ($customer) {
+ $customers = $this->callCustomerFind()->indexBy(function ($customer) {
return $customer->id . '-' . $customer->name;
-// })->orderBy('id')->all();
- })->all();
+ })/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis
$this->assertEquals(3, count($customers));
- $this->assertTrue($customers['1-user1'] instanceof Customer);
- $this->assertTrue($customers['2-user2'] instanceof Customer);
- $this->assertTrue($customers['3-user3'] instanceof Customer);
- }
-
- public function testFindCount()
- {
- $this->assertEquals(3, Customer::find()->count());
- $this->assertEquals(1, Customer::find()->limit(1)->count());
- $this->assertEquals(2, Customer::find()->limit(2)->count());
- $this->assertEquals(1, Customer::find()->offset(2)->limit(2)->count());
+ $this->assertTrue($customers['1-user1'] instanceof $customerClass);
+ $this->assertTrue($customers['2-user2'] instanceof $customerClass);
+ $this->assertTrue($customers['3-user3'] instanceof $customerClass);
}
public function testFindLimit()
{
+ // TODO this test is duplicated because of missing orderBy support in redis
+ /** @var TestCase|ActiveRecordTestTrait $this */
// all()
- $customers = Customer::find()->all();
+ $customers = $this->callCustomerFind()->all();
$this->assertEquals(3, count($customers));
- $customers = Customer::find()->limit(1)->all();
+ $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->all();
$this->assertEquals(1, count($customers));
$this->assertEquals('user1', $customers[0]->name);
- $customers = Customer::find()->limit(1)->offset(1)->all();
+ $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(1)->all();
$this->assertEquals(1, count($customers));
$this->assertEquals('user2', $customers[0]->name);
- $customers = Customer::find()->limit(1)->offset(2)->all();
+ $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(1)->offset(2)->all();
$this->assertEquals(1, count($customers));
$this->assertEquals('user3', $customers[0]->name);
- $customers = Customer::find()->limit(2)->offset(1)->all();
+ $customers = $this->callCustomerFind()/*->orderBy('id')*/->limit(2)->offset(1)->all();
$this->assertEquals(2, count($customers));
$this->assertEquals('user2', $customers[0]->name);
$this->assertEquals('user3', $customers[1]->name);
- $customers = Customer::find()->limit(2)->offset(3)->all();
+ $customers = $this->callCustomerFind()->limit(2)->offset(3)->all();
$this->assertEquals(0, count($customers));
// one()
- $customer = Customer::find()->one();
+ $customer = $this->callCustomerFind()/*->orderBy('id')*/->one();
$this->assertEquals('user1', $customer->name);
- $customer = Customer::find()->offset(0)->one();
+ $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(0)->one();
$this->assertEquals('user1', $customer->name);
- $customer = Customer::find()->offset(1)->one();
+ $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(1)->one();
$this->assertEquals('user2', $customer->name);
- $customer = Customer::find()->offset(2)->one();
+ $customer = $this->callCustomerFind()/*->orderBy('id')*/->offset(2)->one();
$this->assertEquals('user3', $customer->name);
- $customer = Customer::find()->offset(3)->one();
+ $customer = $this->callCustomerFind()->offset(3)->one();
$this->assertNull($customer);
-
- }
-
- public function testFindComplexCondition()
- {
- $this->assertEquals(2, Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->count());
- $this->assertEquals(2, count(Customer::find()->where(['OR', ['id' => 1], ['id' => 2]])->all()));
-
- $this->assertEquals(2, Customer::find()->where(['id' => [1,2]])->count());
- $this->assertEquals(2, count(Customer::find()->where(['id' => [1,2]])->all()));
-
- $this->assertEquals(1, Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->count());
- $this->assertEquals(1, count(Customer::find()->where(['AND', ['id' => [2,3]], ['BETWEEN', 'status', 2, 4]])->all()));
- }
-
- public function testSum()
- {
- $this->assertEquals(6, OrderItem::find()->count());
- $this->assertEquals(7, OrderItem::find()->sum('quantity'));
- }
-
- public function testFindColumn()
- {
- $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name'));
-// TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name'));
- }
-
- public function testExists()
- {
- $this->assertTrue(Customer::find()->where(['id' => 2])->exists());
- $this->assertFalse(Customer::find()->where(['id' => 5])->exists());
- }
-
- public function testFindLazy()
- {
- /** @var $customer Customer */
- $customer = Customer::find(2);
- $orders = $customer->orders;
- $this->assertEquals(2, count($orders));
-
- $orders = $customer->getOrders()->where(['id' => 3])->all();
- $this->assertEquals(1, count($orders));
- $this->assertEquals(3, $orders[0]->id);
- }
-
- public function testFindEager()
- {
- $customers = Customer::find()->with('orders')->all();
- $this->assertEquals(3, count($customers));
- $this->assertEquals(1, count($customers[0]->orders));
- $this->assertEquals(2, count($customers[1]->orders));
- }
-
- public function testFindLazyVia()
- {
- /** @var $order Order */
- $order = Order::find(1);
- $this->assertEquals(1, $order->id);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(1, $order->items[0]->id);
- $this->assertEquals(2, $order->items[1]->id);
-
- $order = Order::find(1);
- $order->id = 100;
- $this->assertEquals([], $order->items);
}
public function testFindEagerViaRelation()
{
- $orders = Order::find()->with('items')->all();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $orders = $this->callOrderFind()->with('items')/*->orderBy('id')*/->all(); // TODO this test is duplicated because of missing orderBy support in redis
$this->assertEquals(3, count($orders));
$order = $orders[0];
$this->assertEquals(1, $order->id);
@@ -287,147 +205,22 @@ class ActiveRecordTest extends RedisTestCase
$this->assertEquals(2, $order->items[1]->id);
}
- public function testFindNestedRelation()
- {
- $customers = Customer::find()->with('orders', 'orders.items')->all();
- $this->assertEquals(3, count($customers));
- $this->assertEquals(1, count($customers[0]->orders));
- $this->assertEquals(2, count($customers[1]->orders));
- $this->assertEquals(0, count($customers[2]->orders));
- $this->assertEquals(2, count($customers[0]->orders[0]->items));
- $this->assertEquals(3, count($customers[1]->orders[0]->items));
- $this->assertEquals(1, count($customers[1]->orders[1]->items));
- }
-
- public function testLink()
- {
- $customer = Customer::find(2);
- $this->assertEquals(2, count($customer->orders));
-
- // has many
- $order = new Order;
- $order->total = 100;
- $this->assertTrue($order->isNewRecord);
- $customer->link('orders', $order);
- $this->assertEquals(3, count($customer->orders));
- $this->assertFalse($order->isNewRecord);
- $this->assertEquals(3, count($customer->getOrders()->all()));
- $this->assertEquals(2, $order->customer_id);
-
- // belongs to
- $order = new Order;
- $order->total = 100;
- $this->assertTrue($order->isNewRecord);
- $customer = Customer::find(1);
- $this->assertNull($order->customer);
- $order->link('customer', $customer);
- $this->assertFalse($order->isNewRecord);
- $this->assertEquals(1, $order->customer_id);
- $this->assertEquals(1, $order->customer->id);
-
- // via model
- $order = Order::find(1);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(2, count($order->orderItems));
- $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]);
- $this->assertNull($orderItem);
- $item = Item::find(3);
- $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]);
- $this->assertEquals(3, count($order->items));
- $this->assertEquals(3, count($order->orderItems));
- $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]);
- $this->assertTrue($orderItem instanceof OrderItem);
- $this->assertEquals(10, $orderItem->quantity);
- $this->assertEquals(100, $orderItem->subtotal);
- }
-
- public function testUnlink()
+ public function testFindCount()
{
- // has many
- $customer = Customer::find(2);
- $this->assertEquals(2, count($customer->orders));
- $customer->unlink('orders', $customer->orders[1], true);
- $this->assertEquals(1, count($customer->orders));
- $this->assertNull(Order::find(3));
-
- // via model
- $order = Order::find(2);
- $this->assertEquals(3, count($order->items));
- $this->assertEquals(3, count($order->orderItems));
- $order->unlink('items', $order->items[2], true);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(2, count($order->orderItems));
+ $this->assertEquals(3, Customer::find()->count());
+ $this->assertEquals(1, Customer::find()->limit(1)->count());
+ $this->assertEquals(2, Customer::find()->limit(2)->count());
+ $this->assertEquals(1, Customer::find()->offset(2)->limit(2)->count());
}
- public function testInsert()
+ public function testFindColumn()
{
- $customer = new Customer;
- $customer->email = 'user4@example.com';
- $customer->name = 'user4';
- $customer->address = 'address4';
-
- $this->assertNull($customer->id);
- $this->assertTrue($customer->isNewRecord);
-
- $customer->save();
-
- $this->assertEquals(4, $customer->id);
- $this->assertFalse($customer->isNewRecord);
+ $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->column('name'));
+// TODO $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->column('name'));
}
// TODO test serial column incr
- public function testUpdate()
- {
- // save
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $this->assertFalse($customer->isNewRecord);
- $customer->name = 'user2x';
- $customer->save();
- $this->assertEquals('user2x', $customer->name);
- $this->assertFalse($customer->isNewRecord);
- $customer2 = Customer::find(2);
- $this->assertEquals('user2x', $customer2->name);
-
- // updateAll
- $customer = Customer::find(3);
- $this->assertEquals('user3', $customer->name);
- $ret = Customer::updateAll(array(
- 'name' => 'temp',
- ), ['id' => 3]);
- $this->assertEquals(1, $ret);
- $customer = Customer::find(3);
- $this->assertEquals('temp', $customer->name);
- }
-
- public function testUpdateCounters()
- {
- // updateCounters
- $pk = ['order_id' => 2, 'item_id' => 4];
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(1, $orderItem->quantity);
- $ret = $orderItem->updateCounters(['quantity' => -1]);
- $this->assertTrue($ret);
- $this->assertEquals(0, $orderItem->quantity);
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(0, $orderItem->quantity);
-
- // updateAllCounters
- $pk = ['order_id' => 1, 'item_id' => 2];
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(2, $orderItem->quantity);
- $ret = OrderItem::updateAllCounters(array(
- 'quantity' => 3,
- 'subtotal' => -10,
- ), $pk);
- $this->assertEquals(1, $ret);
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(5, $orderItem->quantity);
- $this->assertEquals(30, $orderItem->subtotal);
- }
-
public function testUpdatePk()
{
// updateCounters
@@ -443,23 +236,4 @@ class ActiveRecordTest extends RedisTestCase
$this->assertNull(OrderItem::find($pk));
$this->assertNotNull(OrderItem::find(['order_id' => 2, 'item_id' => 10]));
}
-
- public function testDelete()
- {
- // delete
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer->delete();
- $customer = Customer::find(2);
- $this->assertNull($customer);
-
- // deleteAll
- $customers = Customer::find()->all();
- $this->assertEquals(2, count($customers));
- $ret = Customer::deleteAll();
- $this->assertEquals(2, $ret);
- $customers = Customer::find()->all();
- $this->assertEquals(0, count($customers));
- }
}
\ No newline at end of file
diff --git a/tests/unit/extensions/swiftmailer/MailerTest.php b/tests/unit/extensions/swiftmailer/MailerTest.php
index dbc93f3..ce88711 100644
--- a/tests/unit/extensions/swiftmailer/MailerTest.php
+++ b/tests/unit/extensions/swiftmailer/MailerTest.php
@@ -4,9 +4,10 @@ namespace yiiunit\extensions\swiftmailer;
use Yii;
use yii\swiftmailer\Mailer;
-use yii\swiftmailer\Message;
use yiiunit\VendorTestCase;
+Yii::setAlias('@yii/swiftmailer', __DIR__ . '/../../../../extensions/swiftmailer');
+
/**
* @group vendor
* @group mail
diff --git a/tests/unit/extensions/swiftmailer/MessageTest.php b/tests/unit/extensions/swiftmailer/MessageTest.php
index 6309f15..1f55cc6 100644
--- a/tests/unit/extensions/swiftmailer/MessageTest.php
+++ b/tests/unit/extensions/swiftmailer/MessageTest.php
@@ -8,6 +8,8 @@ use yii\swiftmailer\Mailer;
use yii\swiftmailer\Message;
use yiiunit\VendorTestCase;
+Yii::setAlias('@yii/swiftmailer', __DIR__ . '/../../../../extensions/swiftmailer');
+
/**
* @group vendor
* @group mail
diff --git a/tests/unit/framework/ar/ActiveRecordTestTrait.php b/tests/unit/framework/ar/ActiveRecordTestTrait.php
new file mode 100644
index 0000000..75e2120
--- /dev/null
+++ b/tests/unit/framework/ar/ActiveRecordTestTrait.php
@@ -0,0 +1,749 @@
+
+ */
+
+namespace yiiunit\framework\ar;
+
+use yii\db\ActiveQueryInterface;
+use yiiunit\TestCase;
+use yiiunit\data\ar\Customer;
+use yiiunit\data\ar\Order;
+
+/**
+ * This trait provides unit tests shared by the differen AR implementations
+ *
+ * @var TestCase $this
+ */
+trait ActiveRecordTestTrait
+{
+ /**
+ * This method should call Customer::find($q)
+ * @param $q
+ * @return mixed
+ */
+ public abstract function callCustomerFind($q = null);
+
+ /**
+ * This method should call Order::find($q)
+ * @param $q
+ * @return mixed
+ */
+ public abstract function callOrderFind($q = null);
+
+ /**
+ * This method should call OrderItem::find($q)
+ * @param $q
+ * @return mixed
+ */
+ public abstract function callOrderItemFind($q = null);
+
+ /**
+ * This method should call Item::find($q)
+ * @param $q
+ * @return mixed
+ */
+ public abstract function callItemFind($q = null);
+
+ /**
+ * This method should return the classname of Customer class
+ * @return string
+ */
+ public abstract function getCustomerClass();
+
+ /**
+ * This method should return the classname of Order class
+ * @return string
+ */
+ public abstract function getOrderClass();
+
+ /**
+ * This method should return the classname of OrderItem class
+ * @return string
+ */
+ public abstract function getOrderItemClass();
+
+ /**
+ * This method should return the classname of Item class
+ * @return string
+ */
+ public abstract function getItemClass();
+
+ /**
+ * can be overridden to do things after save()
+ */
+ public function afterSave()
+ {
+ }
+
+
+ public function testFind()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // find one
+ $result = $this->callCustomerFind();
+ $this->assertTrue($result instanceof ActiveQueryInterface);
+ $customer = $result->one();
+ $this->assertTrue($customer instanceof $customerClass);
+
+ // find all
+ $customers = $this->callCustomerFind()->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers[0] instanceof $customerClass);
+ $this->assertTrue($customers[1] instanceof $customerClass);
+ $this->assertTrue($customers[2] instanceof $customerClass);
+
+ // find all asArray
+ $customers = $this->callCustomerFind()->asArray()->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers[0]);
+ $this->assertArrayHasKey('name', $customers[0]);
+ $this->assertArrayHasKey('email', $customers[0]);
+ $this->assertArrayHasKey('address', $customers[0]);
+ $this->assertArrayHasKey('status', $customers[0]);
+ $this->assertArrayHasKey('id', $customers[1]);
+ $this->assertArrayHasKey('name', $customers[1]);
+ $this->assertArrayHasKey('email', $customers[1]);
+ $this->assertArrayHasKey('address', $customers[1]);
+ $this->assertArrayHasKey('status', $customers[1]);
+ $this->assertArrayHasKey('id', $customers[2]);
+ $this->assertArrayHasKey('name', $customers[2]);
+ $this->assertArrayHasKey('email', $customers[2]);
+ $this->assertArrayHasKey('address', $customers[2]);
+ $this->assertArrayHasKey('status', $customers[2]);
+
+ // find by a single primary key
+ $customer = $this->callCustomerFind(2);
+ $this->assertTrue($customer instanceof $customerClass);
+ $this->assertEquals('user2', $customer->name);
+ $customer = $this->callCustomerFind(5);
+ $this->assertNull($customer);
+ $customer = $this->callCustomerFind(['id' => [5, 6, 1]]);
+ $this->assertEquals(1, count($customer));
+ $customer = $this->callCustomerFind()->where(['id' => [5, 6, 1]])->one();
+ $this->assertNotNull($customer);
+
+ // find by column values
+ $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user2']);
+ $this->assertTrue($customer instanceof $customerClass);
+ $this->assertEquals('user2', $customer->name);
+ $customer = $this->callCustomerFind(['id' => 2, 'name' => 'user1']);
+ $this->assertNull($customer);
+ $customer = $this->callCustomerFind(['id' => 5]);
+ $this->assertNull($customer);
+ $customer = $this->callCustomerFind(['name' => 'user5']);
+ $this->assertNull($customer);
+
+ // find by attributes
+ $customer = $this->callCustomerFind()->where(['name' => 'user2'])->one();
+ $this->assertTrue($customer instanceof $customerClass);
+ $this->assertEquals(2, $customer->id);
+
+ // scope
+ $this->assertEquals(2, count($this->callCustomerFind()->active()->all()));
+ $this->assertEquals(2, $this->callCustomerFind()->active()->count());
+
+ // asArray
+ $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one();
+ $this->assertEquals([
+ 'id' => '2',
+ 'email' => 'user2@example.com',
+ 'name' => 'user2',
+ 'address' => 'address2',
+ 'status' => '1',
+ ], $customer);
+ }
+
+ public function testFindScalar()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // query scalar
+ $customerName = $this->callCustomerFind()->where(['id' => 2])->scalar('name');
+ $this->assertEquals('user2', $customerName);
+ $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('name');
+ $this->assertEquals('user3', $customerName);
+ $customerName = $this->callCustomerFind()->where(['status' => 2])->scalar('noname');
+ $this->assertNull($customerName);
+ $customerId = $this->callCustomerFind()->where(['status' => 2])->scalar('id');
+ $this->assertEquals(3, $customerId);
+ }
+
+ public function testFindColumn()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $this->assertEquals(['user1', 'user2', 'user3'], $this->callCustomerFind()->orderBy(['name' => SORT_ASC])->column('name'));
+ $this->assertEquals(['user3', 'user2', 'user1'], $this->callCustomerFind()->orderBy(['name' => SORT_DESC])->column('name'));
+ }
+
+ public function testfindIndexBy()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // indexBy
+ $customers = $this->callCustomerFind()->indexBy('name')->orderBy('id')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers['user1'] instanceof $customerClass);
+ $this->assertTrue($customers['user2'] instanceof $customerClass);
+ $this->assertTrue($customers['user3'] instanceof $customerClass);
+
+ // indexBy callable
+ $customers = $this->callCustomerFind()->indexBy(function ($customer) {
+ return $customer->id . '-' . $customer->name;
+ })->orderBy('id')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers['1-user1'] instanceof $customerClass);
+ $this->assertTrue($customers['2-user2'] instanceof $customerClass);
+ $this->assertTrue($customers['3-user3'] instanceof $customerClass);
+ }
+
+ public function testfindIndexByAsArray()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // indexBy + asArray
+ $customers = $this->callCustomerFind()->asArray()->indexBy('name')->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers['user1']);
+ $this->assertArrayHasKey('name', $customers['user1']);
+ $this->assertArrayHasKey('email', $customers['user1']);
+ $this->assertArrayHasKey('address', $customers['user1']);
+ $this->assertArrayHasKey('status', $customers['user1']);
+ $this->assertArrayHasKey('id', $customers['user2']);
+ $this->assertArrayHasKey('name', $customers['user2']);
+ $this->assertArrayHasKey('email', $customers['user2']);
+ $this->assertArrayHasKey('address', $customers['user2']);
+ $this->assertArrayHasKey('status', $customers['user2']);
+ $this->assertArrayHasKey('id', $customers['user3']);
+ $this->assertArrayHasKey('name', $customers['user3']);
+ $this->assertArrayHasKey('email', $customers['user3']);
+ $this->assertArrayHasKey('address', $customers['user3']);
+ $this->assertArrayHasKey('status', $customers['user3']);
+
+ // indexBy callable + asArray
+ $customers = $this->callCustomerFind()->indexBy(function ($customer) {
+ return $customer['id'] . '-' . $customer['name'];
+ })->asArray()->all();
+ $this->assertEquals(3, count($customers));
+ $this->assertArrayHasKey('id', $customers['1-user1']);
+ $this->assertArrayHasKey('name', $customers['1-user1']);
+ $this->assertArrayHasKey('email', $customers['1-user1']);
+ $this->assertArrayHasKey('address', $customers['1-user1']);
+ $this->assertArrayHasKey('status', $customers['1-user1']);
+ $this->assertArrayHasKey('id', $customers['2-user2']);
+ $this->assertArrayHasKey('name', $customers['2-user2']);
+ $this->assertArrayHasKey('email', $customers['2-user2']);
+ $this->assertArrayHasKey('address', $customers['2-user2']);
+ $this->assertArrayHasKey('status', $customers['2-user2']);
+ $this->assertArrayHasKey('id', $customers['3-user3']);
+ $this->assertArrayHasKey('name', $customers['3-user3']);
+ $this->assertArrayHasKey('email', $customers['3-user3']);
+ $this->assertArrayHasKey('address', $customers['3-user3']);
+ $this->assertArrayHasKey('status', $customers['3-user3']);
+ }
+
+ public function testRefresh()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = new $customerClass();
+ $this->assertFalse($customer->refresh());
+
+ $customer = $this->callCustomerFind(1);
+ $customer->name = 'to be refreshed';
+ $this->assertTrue($customer->refresh());
+ $this->assertEquals('user1', $customer->name);
+ }
+
+ public function testEquals()
+ {
+ $customerClass = $this->getCustomerClass();
+ $itemClass = $this->getItemClass();
+
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customerA = new $customerClass();
+ $customerB = new $customerClass();
+ $this->assertFalse($customerA->equals($customerB));
+
+ $customerA = new $customerClass();
+ $customerB = new $itemClass();
+ $this->assertFalse($customerA->equals($customerB));
+
+ $customerA = $this->callCustomerFind(1);
+ $customerB = $this->callCustomerFind(2);
+ $this->assertFalse($customerA->equals($customerB));
+
+ $customerB = $this->callCustomerFind(1);
+ $this->assertTrue($customerA->equals($customerB));
+
+ $customerA = $this->callCustomerFind(1);
+ $customerB = $this->callItemFind(1);
+ $this->assertFalse($customerA->equals($customerB));
+ }
+
+ public function testFindCount()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $this->assertEquals(3, $this->callCustomerFind()->count());
+ // TODO should limit have effect on count()
+// $this->assertEquals(1, $this->callCustomerFind()->limit(1)->count());
+// $this->assertEquals(2, $this->callCustomerFind()->limit(2)->count());
+// $this->assertEquals(1, $this->callCustomerFind()->offset(2)->limit(2)->count());
+ }
+
+ public function testFindLimit()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // all()
+ $customers = $this->callCustomerFind()->all();
+ $this->assertEquals(3, count($customers));
+
+ $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->all();
+ $this->assertEquals(1, count($customers));
+ $this->assertEquals('user1', $customers[0]->name);
+
+ $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(1)->all();
+ $this->assertEquals(1, count($customers));
+ $this->assertEquals('user2', $customers[0]->name);
+
+ $customers = $this->callCustomerFind()->orderBy('id')->limit(1)->offset(2)->all();
+ $this->assertEquals(1, count($customers));
+ $this->assertEquals('user3', $customers[0]->name);
+
+ $customers = $this->callCustomerFind()->orderBy('id')->limit(2)->offset(1)->all();
+ $this->assertEquals(2, count($customers));
+ $this->assertEquals('user2', $customers[0]->name);
+ $this->assertEquals('user3', $customers[1]->name);
+
+ $customers = $this->callCustomerFind()->limit(2)->offset(3)->all();
+ $this->assertEquals(0, count($customers));
+
+ // one()
+ $customer = $this->callCustomerFind()->orderBy('id')->one();
+ $this->assertEquals('user1', $customer->name);
+
+ $customer = $this->callCustomerFind()->orderBy('id')->offset(0)->one();
+ $this->assertEquals('user1', $customer->name);
+
+ $customer = $this->callCustomerFind()->orderBy('id')->offset(1)->one();
+ $this->assertEquals('user2', $customer->name);
+
+ $customer = $this->callCustomerFind()->orderBy('id')->offset(2)->one();
+ $this->assertEquals('user3', $customer->name);
+
+ $customer = $this->callCustomerFind()->offset(3)->one();
+ $this->assertNull($customer);
+
+ }
+
+ public function testFindComplexCondition()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $this->assertEquals(2, $this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->count());
+ $this->assertEquals(2, count($this->callCustomerFind()->where(['OR', ['name' => 'user1'], ['name' => 'user2']])->all()));
+
+ $this->assertEquals(2, $this->callCustomerFind()->where(['name' => ['user1','user2']])->count());
+ $this->assertEquals(2, count($this->callCustomerFind()->where(['name' => ['user1','user2']])->all()));
+
+ $this->assertEquals(1, $this->callCustomerFind()->where(['AND', ['name' => ['user2','user3']], ['BETWEEN', 'status', 2, 4]])->count());
+ $this->assertEquals(1, count($this->callCustomerFind()->where(['AND', ['name' => ['user2','user3']], ['BETWEEN', 'status', 2, 4]])->all()));
+ }
+
+ public function testFindNullValues()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = $this->callCustomerFind(2);
+ $customer->name = null;
+ $customer->save(false);
+ $this->afterSave();
+
+ $result = $this->callCustomerFind()->where(['name' => null])->all();
+ $this->assertEquals(1, count($result));
+ $this->assertEquals(2, reset($result)->primaryKey);
+ }
+
+ public function testExists()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $this->assertTrue($this->callCustomerFind()->where(['id' => 2])->exists());
+ $this->assertFalse($this->callCustomerFind()->where(['id' => 5])->exists());
+ $this->assertTrue($this->callCustomerFind()->where(['name' => 'user1'])->exists());
+ $this->assertFalse($this->callCustomerFind()->where(['name' => 'user5'])->exists());
+ }
+
+ public function testFindLazy()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = $this->callCustomerFind(2);
+ $this->assertFalse($customer->isRelationPopulated('orders'));
+ $orders = $customer->orders;
+ $this->assertTrue($customer->isRelationPopulated('orders'));
+ $this->assertEquals(2, count($orders));
+ $this->assertEquals(1, count($customer->populatedRelations));
+
+ /** @var Customer $customer */
+ $customer = $this->callCustomerFind(2);
+ $this->assertFalse($customer->isRelationPopulated('orders'));
+ $orders = $customer->getOrders()->where(['id' => 3])->all();
+ $this->assertFalse($customer->isRelationPopulated('orders'));
+ $this->assertEquals(0, count($customer->populatedRelations));
+
+ $this->assertEquals(1, count($orders));
+ $this->assertEquals(3, $orders[0]->id);
+ }
+
+ public function testFindEager()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customers = $this->callCustomerFind()->with('orders')->indexBy('id')->all();
+ ksort($customers);
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers[1]->isRelationPopulated('orders'));
+ $this->assertTrue($customers[2]->isRelationPopulated('orders'));
+ $this->assertTrue($customers[3]->isRelationPopulated('orders'));
+ $this->assertEquals(1, count($customers[1]->orders));
+ $this->assertEquals(2, count($customers[2]->orders));
+ $this->assertEquals(0, count($customers[3]->orders));
+
+ $customer = $this->callCustomerFind()->where(['id' => 1])->with('orders')->one();
+ $this->assertTrue($customer->isRelationPopulated('orders'));
+ $this->assertEquals(1, count($customer->orders));
+ $this->assertEquals(1, count($customer->populatedRelations));
+ }
+
+ public function testFindLazyVia()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ /** @var Order $order */
+ $order = $this->callOrderFind(1);
+ $this->assertEquals(1, $order->id);
+ $this->assertEquals(2, count($order->items));
+ $this->assertEquals(1, $order->items[0]->id);
+ $this->assertEquals(2, $order->items[1]->id);
+ }
+
+ public function testFindLazyVia2()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ /** @var Order $order */
+ $order = $this->callOrderFind(1);
+ $order->id = 100;
+ $this->assertEquals([], $order->items);
+ }
+
+ public function testFindEagerViaRelation()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $orders = $this->callOrderFind()->with('items')->orderBy('id')->all();
+ $this->assertEquals(3, count($orders));
+ $order = $orders[0];
+ $this->assertEquals(1, $order->id);
+ $this->assertTrue($order->isRelationPopulated('items'));
+ $this->assertEquals(2, count($order->items));
+ $this->assertEquals(1, $order->items[0]->id);
+ $this->assertEquals(2, $order->items[1]->id);
+ }
+
+ public function testFindNestedRelation()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customers = $this->callCustomerFind()->with('orders', 'orders.items')->indexBy('id')->all();
+ ksort($customers);
+ $this->assertEquals(3, count($customers));
+ $this->assertTrue($customers[1]->isRelationPopulated('orders'));
+ $this->assertTrue($customers[2]->isRelationPopulated('orders'));
+ $this->assertTrue($customers[3]->isRelationPopulated('orders'));
+ $this->assertEquals(1, count($customers[1]->orders));
+ $this->assertEquals(2, count($customers[2]->orders));
+ $this->assertEquals(0, count($customers[3]->orders));
+ $this->assertTrue($customers[1]->orders[0]->isRelationPopulated('items'));
+ $this->assertTrue($customers[2]->orders[0]->isRelationPopulated('items'));
+ $this->assertTrue($customers[2]->orders[1]->isRelationPopulated('items'));
+ $this->assertEquals(2, count($customers[1]->orders[0]->items));
+ $this->assertEquals(3, count($customers[2]->orders[0]->items));
+ $this->assertEquals(1, count($customers[2]->orders[1]->items));
+ }
+
+ /**
+ * Ensure ActiveRelation does preserve order of items on find via()
+ * https://github.com/yiisoft/yii2/issues/1310
+ */
+ public function testFindEagerViaRelationPreserveOrder()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('create_time')->all();
+ $this->assertEquals(3, count($orders));
+
+ $order = $orders[0];
+ $this->assertEquals(1, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder1'));
+ $this->assertEquals(2, count($order->itemsInOrder1));
+ $this->assertEquals(1, $order->itemsInOrder1[0]->id);
+ $this->assertEquals(2, $order->itemsInOrder1[1]->id);
+
+ $order = $orders[1];
+ $this->assertEquals(2, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder1'));
+ $this->assertEquals(3, count($order->itemsInOrder1));
+ $this->assertEquals(5, $order->itemsInOrder1[0]->id);
+ $this->assertEquals(3, $order->itemsInOrder1[1]->id);
+ $this->assertEquals(4, $order->itemsInOrder1[2]->id);
+
+ $order = $orders[2];
+ $this->assertEquals(3, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder1'));
+ $this->assertEquals(1, count($order->itemsInOrder1));
+ $this->assertEquals(2, $order->itemsInOrder1[0]->id);
+ }
+
+ // different order in via table
+ public function testFindEagerViaRelationPreserveOrderB()
+ {
+ $orders = $this->callOrderFind()->with('itemsInOrder2')->orderBy('create_time')->all();
+ $this->assertEquals(3, count($orders));
+
+ $order = $orders[0];
+ $this->assertEquals(1, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder2'));
+ $this->assertEquals(2, count($order->itemsInOrder2));
+ $this->assertEquals(1, $order->itemsInOrder2[0]->id);
+ $this->assertEquals(2, $order->itemsInOrder2[1]->id);
+
+ $order = $orders[1];
+ $this->assertEquals(2, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder2'));
+ $this->assertEquals(3, count($order->itemsInOrder2));
+ $this->assertEquals(5, $order->itemsInOrder2[0]->id);
+ $this->assertEquals(3, $order->itemsInOrder2[1]->id);
+ $this->assertEquals(4, $order->itemsInOrder2[2]->id);
+
+ $order = $orders[2];
+ $this->assertEquals(3, $order->id);
+ $this->assertTrue($order->isRelationPopulated('itemsInOrder2'));
+ $this->assertEquals(1, count($order->itemsInOrder2));
+ $this->assertEquals(2, $order->itemsInOrder2[0]->id);
+ }
+
+ public function testLink()
+ {
+ $orderClass = $this->getOrderClass();
+ $orderItemClass = $this->getOrderItemClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = $this->callCustomerFind(2);
+ $this->assertEquals(2, count($customer->orders));
+
+ // has many
+ $order = new $orderClass;
+ $order->total = 100;
+ $this->assertTrue($order->isNewRecord);
+ $customer->link('orders', $order);
+ $this->afterSave();
+ $this->assertEquals(3, count($customer->orders));
+ $this->assertFalse($order->isNewRecord);
+ $this->assertEquals(3, count($customer->getOrders()->all()));
+ $this->assertEquals(2, $order->customer_id);
+
+ // belongs to
+ $order = new $orderClass;
+ $order->total = 100;
+ $this->assertTrue($order->isNewRecord);
+ $customer = $this->callCustomerFind(1);
+ $this->assertNull($order->customer);
+ $order->link('customer', $customer);
+ $this->assertFalse($order->isNewRecord);
+ $this->assertEquals(1, $order->customer_id);
+ $this->assertEquals(1, $order->customer->primaryKey);
+
+ // via model
+ $order = $this->callOrderFind(1);
+ $this->assertEquals(2, count($order->items));
+ $this->assertEquals(2, count($order->orderItems));
+ $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]);
+ $this->assertNull($orderItem);
+ $item = $this->callItemFind(3);
+ $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]);
+ $this->afterSave();
+ $this->assertEquals(3, count($order->items));
+ $this->assertEquals(3, count($order->orderItems));
+ $orderItem = $this->callOrderItemFind(['order_id' => 1, 'item_id' => 3]);
+ $this->assertTrue($orderItem instanceof $orderItemClass);
+ $this->assertEquals(10, $orderItem->quantity);
+ $this->assertEquals(100, $orderItem->subtotal);
+ }
+
+ public function testUnlink()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // has many
+ $customer = $this->callCustomerFind(2);
+ $this->assertEquals(2, count($customer->orders));
+ $customer->unlink('orders', $customer->orders[1], true);
+ $this->afterSave();
+ $this->assertEquals(1, count($customer->orders));
+ $this->assertNull($this->callOrderFind(3));
+
+ // via model
+ $order = $this->callOrderFind(2);
+ $this->assertEquals(3, count($order->items));
+ $this->assertEquals(3, count($order->orderItems));
+ $order->unlink('items', $order->items[2], true);
+ $this->afterSave();
+ $this->assertEquals(2, count($order->items));
+ $this->assertEquals(2, count($order->orderItems));
+ }
+
+ public static $afterSaveNewRecord;
+ public static $afterSaveInsert;
+
+ public function testInsert()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = new $customerClass;
+ $customer->email = 'user4@example.com';
+ $customer->name = 'user4';
+ $customer->address = 'address4';
+
+ $this->assertNull($customer->id);
+ $this->assertTrue($customer->isNewRecord);
+ static::$afterSaveNewRecord = null;
+ static::$afterSaveInsert = null;
+
+ $customer->save();
+ $this->afterSave();
+
+ $this->assertNotNull($customer->id);
+ $this->assertFalse(static::$afterSaveNewRecord);
+ $this->assertTrue(static::$afterSaveInsert);
+ $this->assertFalse($customer->isNewRecord);
+ }
+
+ public function testUpdate()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // save
+ $customer = $this->callCustomerFind(2);
+ $this->assertTrue($customer instanceof $customerClass);
+ $this->assertEquals('user2', $customer->name);
+ $this->assertFalse($customer->isNewRecord);
+ static::$afterSaveNewRecord = null;
+ static::$afterSaveInsert = null;
+
+ $customer->name = 'user2x';
+ $customer->save();
+ $this->afterSave();
+ $this->assertEquals('user2x', $customer->name);
+ $this->assertFalse($customer->isNewRecord);
+ $this->assertFalse(static::$afterSaveNewRecord);
+ $this->assertFalse(static::$afterSaveInsert);
+ $customer2 = $this->callCustomerFind(2);
+ $this->assertEquals('user2x', $customer2->name);
+
+ // updateAll
+ $customer = $this->callCustomerFind(3);
+ $this->assertEquals('user3', $customer->name);
+ $ret = $customerClass::updateAll(['name' => 'temp'], ['id' => 3]);
+ $this->afterSave();
+ $this->assertEquals(1, $ret);
+ $customer = $this->callCustomerFind(3);
+ $this->assertEquals('temp', $customer->name);
+
+ $ret = $customerClass::updateAll(['name' => 'tempX']);
+ $this->afterSave();
+ $this->assertEquals(3, $ret);
+
+ $ret = $customerClass::updateAll(['name' => 'temp'], ['name' => 'user6']);
+ $this->afterSave();
+ $this->assertEquals(0, $ret);
+ }
+
+ public function testUpdateCounters()
+ {
+ $orderItemClass = $this->getOrderItemClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // updateCounters
+ $pk = ['order_id' => 2, 'item_id' => 4];
+ $orderItem = $this->callOrderItemFind($pk);
+ $this->assertEquals(1, $orderItem->quantity);
+ $ret = $orderItem->updateCounters(['quantity' => -1]);
+ $this->afterSave();
+ $this->assertTrue($ret);
+ $this->assertEquals(0, $orderItem->quantity);
+ $orderItem = $this->callOrderItemFind($pk);
+ $this->assertEquals(0, $orderItem->quantity);
+
+ // updateAllCounters
+ $pk = ['order_id' => 1, 'item_id' => 2];
+ $orderItem = $this->callOrderItemFind($pk);
+ $this->assertEquals(2, $orderItem->quantity);
+ $ret = $orderItemClass::updateAllCounters([
+ 'quantity' => 3,
+ 'subtotal' => -10,
+ ], $pk);
+ $this->afterSave();
+ $this->assertEquals(1, $ret);
+ $orderItem = $this->callOrderItemFind($pk);
+ $this->assertEquals(5, $orderItem->quantity);
+ $this->assertEquals(30, $orderItem->subtotal);
+ }
+
+ public function testDelete()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ // delete
+ $customer = $this->callCustomerFind(2);
+ $this->assertTrue($customer instanceof $customerClass);
+ $this->assertEquals('user2', $customer->name);
+ $customer->delete();
+ $this->afterSave();
+ $customer = $this->callCustomerFind(2);
+ $this->assertNull($customer);
+
+ // deleteAll
+ $customers = $this->callCustomerFind()->all();
+ $this->assertEquals(2, count($customers));
+ $ret = $customerClass::deleteAll();
+ $this->afterSave();
+ $this->assertEquals(2, $ret);
+ $customers = $this->callCustomerFind()->all();
+ $this->assertEquals(0, count($customers));
+
+ $ret = $customerClass::deleteAll();
+ $this->afterSave();
+ $this->assertEquals(0, $ret);
+ }
+
+ /**
+ * Some PDO implementations(e.g. cubrid) do not support boolean values.
+ * Make sure this does not affect AR layer.
+ */
+ public function testBooleanAttribute()
+ {
+ $customerClass = $this->getCustomerClass();
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $customer = new $customerClass();
+ $customer->name = 'boolean customer';
+ $customer->email = 'mail@example.com';
+ $customer->status = true;
+ $customer->save(false);
+
+ $customer->refresh();
+ $this->assertEquals(1, $customer->status);
+
+ $customer->status = false;
+ $customer->save(false);
+
+ $customer->refresh();
+ $this->assertEquals(0, $customer->status);
+
+ $customers = $this->callCustomerFind()->where(['status' => true])->all();
+ $this->assertEquals(2, count($customers));
+
+ $customers = $this->callCustomerFind()->where(['status' => false])->all();
+ $this->assertEquals(1, count($customers));
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/framework/base/ExceptionTest.php b/tests/unit/framework/base/ExceptionTest.php
index 5a623b1..635b55c 100644
--- a/tests/unit/framework/base/ExceptionTest.php
+++ b/tests/unit/framework/base/ExceptionTest.php
@@ -8,7 +8,7 @@ use yii\base\InvalidCallException;
class ExceptionTest extends TestCase
{
- public function testToArrayWithPrevious()
+ public function testToArrayWithPrevious()
{
$e = new InvalidCallException('bar', 0 ,new InvalidCallException('foo'));
$array = $e->toArray();
diff --git a/tests/unit/framework/data/ActiveDataProviderTest.php b/tests/unit/framework/data/ActiveDataProviderTest.php
index 4e4ca77..a58deab 100644
--- a/tests/unit/framework/data/ActiveDataProviderTest.php
+++ b/tests/unit/framework/data/ActiveDataProviderTest.php
@@ -160,7 +160,7 @@ class ActiveDataProviderTest extends DatabaseTestCase
$this->assertEquals(2, count($provider->getModels()));
}
- public function testPaginationBeforeModels()
+ public function testPaginationBeforeModels()
{
$query = new Query;
$provider = new ActiveDataProvider([
diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php
index 3de40dd..67d107a 100644
--- a/tests/unit/framework/db/ActiveRecordTest.php
+++ b/tests/unit/framework/db/ActiveRecordTest.php
@@ -8,6 +8,7 @@ use yiiunit\data\ar\NullValues;
use yiiunit\data\ar\OrderItem;
use yiiunit\data\ar\Order;
use yiiunit\data\ar\Item;
+use yiiunit\framework\ar\ActiveRecordTestTrait;
/**
* @group db
@@ -15,93 +16,57 @@ use yiiunit\data\ar\Item;
*/
class ActiveRecordTest extends DatabaseTestCase
{
+ use ActiveRecordTestTrait;
+
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
}
- public function testFind()
- {
- // find one
- $result = Customer::find();
- $this->assertTrue($result instanceof ActiveQuery);
- $customer = $result->one();
- $this->assertTrue($customer instanceof Customer);
+ public function callCustomerFind($q = null) { return Customer::find($q); }
+ public function callOrderFind($q = null) { return Order::find($q); }
+ public function callOrderItemFind($q = null) { return OrderItem::find($q); }
+ public function callItemFind($q = null) { return Item::find($q); }
- // find all
- $customers = Customer::find()->all();
- $this->assertEquals(3, count($customers));
- $this->assertTrue($customers[0] instanceof Customer);
- $this->assertTrue($customers[1] instanceof Customer);
- $this->assertTrue($customers[2] instanceof Customer);
-
- // find by a single primary key
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer = Customer::find(5);
- $this->assertNull($customer);
-
- // query scalar
- $customerName = Customer::find()->where(array('id' => 2))->select('name')->scalar();
- $this->assertEquals('user2', $customerName);
-
- // find by column values
- $customer = Customer::find(['id' => 2, 'name' => 'user2']);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer = Customer::find(['id' => 2, 'name' => 'user1']);
- $this->assertNull($customer);
-
- // find by attributes
- $customer = Customer::find()->where(['name' => 'user2'])->one();
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals(2, $customer->id);
+ public function getCustomerClass() { return Customer::className(); }
+ public function getItemClass() { return Item::className(); }
+ public function getOrderClass() { return Order::className(); }
+ public function getOrderItemClass() { return OrderItem::className(); }
+ public function testCustomColumns()
+ {
// find custom column
- $customer = Customer::find()->select(['*', '(status*2) AS status2'])
+ $customer = $this->callCustomerFind()->select(['*', '(status*2) AS status2'])
->where(['name' => 'user3'])->one();
$this->assertEquals(3, $customer->id);
$this->assertEquals(4, $customer->status2);
+ }
+ public function testSatisticalFind()
+ {
// find count, sum, average, min, max, scalar
- $this->assertEquals(3, Customer::find()->count());
- $this->assertEquals(2, Customer::find()->where('id=1 OR id=2')->count());
- $this->assertEquals(6, Customer::find()->sum('id'));
- $this->assertEquals(2, Customer::find()->average('id'));
- $this->assertEquals(1, Customer::find()->min('id'));
- $this->assertEquals(3, Customer::find()->max('id'));
- $this->assertEquals(3, Customer::find()->select('COUNT(*)')->scalar());
-
- // scope
- $this->assertEquals(2, Customer::find()->active()->count());
-
- // asArray
- $customer = Customer::find()->where('id=2')->asArray()->one();
- $this->assertEquals([
- 'id' => '2',
- 'email' => 'user2@example.com',
- 'name' => 'user2',
- 'address' => 'address2',
- 'status' => '1',
- ], $customer);
-
- // indexBy
- $customers = Customer::find()->indexBy('name')->orderBy('id')->all();
- $this->assertEquals(3, count($customers));
- $this->assertTrue($customers['user1'] instanceof Customer);
- $this->assertTrue($customers['user2'] instanceof Customer);
- $this->assertTrue($customers['user3'] instanceof Customer);
-
- // indexBy callable
- $customers = Customer::find()->indexBy(function ($customer) {
- return $customer->id . '-' . $customer->name;
- })->orderBy('id')->all();
- $this->assertEquals(3, count($customers));
- $this->assertTrue($customers['1-user1'] instanceof Customer);
- $this->assertTrue($customers['2-user2'] instanceof Customer);
- $this->assertTrue($customers['3-user3'] instanceof Customer);
+ $this->assertEquals(3, $this->callCustomerFind()->count());
+ $this->assertEquals(2, $this->callCustomerFind()->where('id=1 OR id=2')->count());
+ $this->assertEquals(6, $this->callCustomerFind()->sum('id'));
+ $this->assertEquals(2, $this->callCustomerFind()->average('id'));
+ $this->assertEquals(1, $this->callCustomerFind()->min('id'));
+ $this->assertEquals(3, $this->callCustomerFind()->max('id'));
+ $this->assertEquals(3, $this->callCustomerFind()->select('COUNT(*)')->scalar());
+ }
+
+ public function testFindScalar()
+ {
+ // query scalar
+ $customerName = $this->callCustomerFind()->where(array('id' => 2))->select('name')->scalar();
+ $this->assertEquals('user2', $customerName);
+ }
+
+ public function testFindColumn()
+ {
+ /** @var TestCase|ActiveRecordTestTrait $this */
+ $this->assertEquals(['user1', 'user2', 'user3'], Customer::find()->select('name')->column());
+ $this->assertEquals(['user3', 'user2', 'user1'], Customer::find()->orderBy(['name' => SORT_DESC])->select('name')->column());
}
public function testFindBySql()
@@ -121,67 +86,6 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertEquals('user2', $customer->name);
}
- public function testFindLazy()
- {
- /** @var Customer $customer */
- $customer = Customer::find(2);
- $this->assertFalse($customer->isRelationPopulated('orders'));
- $orders = $customer->orders;
- $this->assertTrue($customer->isRelationPopulated('orders'));
- $this->assertEquals(2, count($orders));
- $this->assertEquals(1, count($customer->populatedRelations));
-
- /** @var Customer $customer */
- $customer = Customer::find(2);
- $this->assertFalse($customer->isRelationPopulated('orders'));
- $orders = $customer->getOrders()->where('id=3')->all();
- $this->assertFalse($customer->isRelationPopulated('orders'));
- $this->assertEquals(0, count($customer->populatedRelations));
-
- $this->assertEquals(1, count($orders));
- $this->assertEquals(3, $orders[0]->id);
- }
-
- public function testFindEager()
- {
- $customers = Customer::find()->with('orders')->all();
- $this->assertEquals(3, count($customers));
- $this->assertTrue($customers[0]->isRelationPopulated('orders'));
- $this->assertTrue($customers[1]->isRelationPopulated('orders'));
- $this->assertEquals(1, count($customers[0]->orders));
- $this->assertEquals(2, count($customers[1]->orders));
-
- $customer = Customer::find()->with('orders')->one();
- $this->assertTrue($customer->isRelationPopulated('orders'));
- $this->assertEquals(1, count($customer->orders));
- $this->assertEquals(1, count($customer->populatedRelations));
- }
-
- public function testFindLazyVia()
- {
- /** @var Order $order */
- $order = Order::find(1);
- $this->assertEquals(1, $order->id);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(1, $order->items[0]->id);
- $this->assertEquals(2, $order->items[1]->id);
-
- $order = Order::find(1);
- $order->id = 100;
- $this->assertEquals([], $order->items);
- }
-
- public function testFindEagerViaRelation()
- {
- $orders = Order::find()->with('items')->orderBy('id')->all();
- $this->assertEquals(3, count($orders));
- $order = $orders[0];
- $this->assertEquals(1, $order->id);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(1, $order->items[0]->id);
- $this->assertEquals(2, $order->items[1]->id);
- }
-
public function testFindLazyViaTable()
{
/** @var Order $order */
@@ -217,188 +121,6 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertEquals(2, $order->books[0]->id);
}
- public function testFindNestedRelation()
- {
- $customers = Customer::find()->with('orders', 'orders.items')->all();
- $this->assertEquals(3, count($customers));
- $this->assertEquals(1, count($customers[0]->orders));
- $this->assertEquals(2, count($customers[1]->orders));
- $this->assertEquals(0, count($customers[2]->orders));
- $this->assertEquals(2, count($customers[0]->orders[0]->items));
- $this->assertEquals(3, count($customers[1]->orders[0]->items));
- $this->assertEquals(1, count($customers[1]->orders[1]->items));
- }
-
- public function testLink()
- {
- $customer = Customer::find(2);
- $this->assertEquals(2, count($customer->orders));
-
- // has many
- $order = new Order;
- $order->total = 100;
- $this->assertTrue($order->isNewRecord);
- $customer->link('orders', $order);
- $this->assertEquals(3, count($customer->orders));
- $this->assertFalse($order->isNewRecord);
- $this->assertEquals(3, count($customer->getOrders()->all()));
- $this->assertEquals(2, $order->customer_id);
-
- // belongs to
- $order = new Order;
- $order->total = 100;
- $this->assertTrue($order->isNewRecord);
- $customer = Customer::find(1);
- $this->assertNull($order->customer);
- $order->link('customer', $customer);
- $this->assertFalse($order->isNewRecord);
- $this->assertEquals(1, $order->customer_id);
- $this->assertEquals(1, $order->customer->id);
-
- // via table
- $order = Order::find(2);
- $this->assertEquals(0, count($order->books));
- $orderItem = OrderItem::find(['order_id' => 2, 'item_id' => 1]);
- $this->assertNull($orderItem);
- $item = Item::find(1);
- $order->link('books', $item, ['quantity' => 10, 'subtotal' => 100]);
- $this->assertEquals(1, count($order->books));
- $orderItem = OrderItem::find(['order_id' => 2, 'item_id' => 1]);
- $this->assertTrue($orderItem instanceof OrderItem);
- $this->assertEquals(10, $orderItem->quantity);
- $this->assertEquals(100, $orderItem->subtotal);
-
- // via model
- $order = Order::find(1);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(2, count($order->orderItems));
- $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]);
- $this->assertNull($orderItem);
- $item = Item::find(3);
- $order->link('items', $item, ['quantity' => 10, 'subtotal' => 100]);
- $this->assertEquals(3, count($order->items));
- $this->assertEquals(3, count($order->orderItems));
- $orderItem = OrderItem::find(['order_id' => 1, 'item_id' => 3]);
- $this->assertTrue($orderItem instanceof OrderItem);
- $this->assertEquals(10, $orderItem->quantity);
- $this->assertEquals(100, $orderItem->subtotal);
- }
-
- public function testUnlink()
- {
- // has many
- $customer = Customer::find(2);
- $this->assertEquals(2, count($customer->orders));
- $customer->unlink('orders', $customer->orders[1], true);
- $this->assertEquals(1, count($customer->orders));
- $this->assertNull(Order::find(3));
-
- // via model
- $order = Order::find(2);
- $this->assertEquals(3, count($order->items));
- $this->assertEquals(3, count($order->orderItems));
- $order->unlink('items', $order->items[2], true);
- $this->assertEquals(2, count($order->items));
- $this->assertEquals(2, count($order->orderItems));
-
- // via table
- $order = Order::find(1);
- $this->assertEquals(2, count($order->books));
- $order->unlink('books', $order->books[1], true);
- $this->assertEquals(1, count($order->books));
- $this->assertEquals(1, count($order->orderItems));
- }
-
- public function testInsert()
- {
- $customer = new Customer;
- $customer->email = 'user4@example.com';
- $customer->name = 'user4';
- $customer->address = 'address4';
-
- $this->assertNull($customer->id);
- $this->assertTrue($customer->isNewRecord);
- Customer::$afterSaveNewRecord = null;
- Customer::$afterSaveInsert = null;
-
- $customer->save();
-
- $this->assertEquals(4, $customer->id);
- $this->assertFalse(Customer::$afterSaveNewRecord);
- $this->assertTrue(Customer::$afterSaveInsert);
- $this->assertFalse($customer->isNewRecord);
- }
-
- public function testUpdate()
- {
- // save
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $this->assertFalse($customer->isNewRecord);
- Customer::$afterSaveNewRecord = null;
- Customer::$afterSaveInsert = null;
-
- $customer->name = 'user2x';
- $customer->save();
- $this->assertEquals('user2x', $customer->name);
- $this->assertFalse($customer->isNewRecord);
- $this->assertFalse(Customer::$afterSaveNewRecord);
- $this->assertFalse(Customer::$afterSaveInsert);
- $customer2 = Customer::find(2);
- $this->assertEquals('user2x', $customer2->name);
-
- // updateCounters
- $pk = ['order_id' => 2, 'item_id' => 4];
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(1, $orderItem->quantity);
- $ret = $orderItem->updateCounters(['quantity' => -1]);
- $this->assertTrue($ret);
- $this->assertEquals(0, $orderItem->quantity);
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(0, $orderItem->quantity);
-
- // updateAll
- $customer = Customer::find(3);
- $this->assertEquals('user3', $customer->name);
- $ret = Customer::updateAll(['name' => 'temp'], ['id' => 3]);
- $this->assertEquals(1, $ret);
- $customer = Customer::find(3);
- $this->assertEquals('temp', $customer->name);
-
- // updateCounters
- $pk = ['order_id' => 1, 'item_id' => 2];
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(2, $orderItem->quantity);
- $ret = OrderItem::updateAllCounters([
- 'quantity' => 3,
- 'subtotal' => -10,
- ], $pk);
- $this->assertEquals(1, $ret);
- $orderItem = OrderItem::find($pk);
- $this->assertEquals(5, $orderItem->quantity);
- $this->assertEquals(30, $orderItem->subtotal);
- }
-
- public function testDelete()
- {
- // delete
- $customer = Customer::find(2);
- $this->assertTrue($customer instanceof Customer);
- $this->assertEquals('user2', $customer->name);
- $customer->delete();
- $customer = Customer::find(2);
- $this->assertNull($customer);
-
- // deleteAll
- $customers = Customer::find()->all();
- $this->assertEquals(2, count($customers));
- $ret = Customer::deleteAll();
- $this->assertEquals(2, $ret);
- $customers = Customer::find()->all();
- $this->assertEquals(0, count($customers));
- }
-
public function testStoreNull()
{
$record = new NullValues();
@@ -468,34 +190,6 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertTrue($record->var2 === $record->var3);
}
- /**
- * Some PDO implementations(e.g. cubrid) do not support boolean values.
- * Make sure this does not affect AR layer.
- */
- public function testBooleanAttribute()
- {
- $customer = new Customer();
- $customer->name = 'boolean customer';
- $customer->email = 'mail@example.com';
- $customer->status = true;
- $customer->save(false);
-
- $customer->refresh();
- $this->assertEquals(1, $customer->status);
-
- $customer->status = false;
- $customer->save(false);
-
- $customer->refresh();
- $this->assertEquals(0, $customer->status);
-
- $customers = Customer::find()->where(['status' => true])->all();
- $this->assertEquals(2, count($customers));
-
- $customers = Customer::find()->where(['status' => false])->all();
- $this->assertEquals(1, count($customers));
- }
-
public function testIsPrimaryKey()
{
$this->assertFalse(Customer::isPrimaryKey([]));
diff --git a/tests/unit/framework/validators/UniqueValidatorTest.php b/tests/unit/framework/validators/UniqueValidatorTest.php
index d0a7091..707239c 100644
--- a/tests/unit/framework/validators/UniqueValidatorTest.php
+++ b/tests/unit/framework/validators/UniqueValidatorTest.php
@@ -80,9 +80,9 @@ class UniqueValidatorTest extends DatabaseTestCase
public function testValidateAttributeAttributeNotInTableException()
{
- $this->setExpectedException('yii\base\InvalidConfigException');
+ $this->setExpectedException('yii\db\Exception');
$val = new UniqueValidator();
$m = new ValidatorTestMainModel();
$val->validateAttribute($m, 'testMainVal');
}
-}
\ No newline at end of file
+}
diff --git a/tests/unit/framework/web/AssetBundleTest.php b/tests/unit/framework/web/AssetBundleTest.php
index 24c36d6..d989a13 100644
--- a/tests/unit/framework/web/AssetBundleTest.php
+++ b/tests/unit/framework/web/AssetBundleTest.php
@@ -1,7 +1,7 @@
*/
diff --git a/tests/unit/framework/web/XmlResponseFormatterTest.php b/tests/unit/framework/web/XmlResponseFormatterTest.php
index 800de66..bf1aa29 100644
--- a/tests/unit/framework/web/XmlResponseFormatterTest.php
+++ b/tests/unit/framework/web/XmlResponseFormatterTest.php
@@ -48,7 +48,7 @@ class XmlResponseFormatterTest extends \yiiunit\TestCase
}
/**
- * @param mixed $data the data to be formatted
+ * @param mixed $data the data to be formatted
* @param string $xml the expected XML body
* @dataProvider formatScalarDataProvider
*/