Commit 3d623a00 by Carsten Brandt

Merge pull request #4380 from yiisoft/array-attribute-relations

Array attribute relations
parents 92e131c5 7939a3de
......@@ -76,6 +76,7 @@ Yii Framework 2 Change Log
- Enh #87: Helper `yii\helpers\Security` converted into application component, cryptographic strength improved (klimov-paul)
- Enh #422: Added Support for BIT(M) data type default values in Schema (cebe)
- Enh #1160: Added $strict parameter to Inflector::camel2id() to handle consecutive uppercase chars (schmunk)
- Enh #1249: Added support for Active Record relation via array attributes (klimov-paul, cebe)
- Enh #1452: Added `Module::getInstance()` to allow accessing the module instance from anywhere within the module (qiangxue)
- Enh #2264: `CookieCollection::has()` will return false for expired or removed cookies (qiangxue)
- Enh #2435: `yii\db\IntegrityException` is now thrown on database integrity errors instead of general `yii\db\Exception` (samdark)
......
......@@ -62,6 +62,7 @@ trait ActiveRelationTrait
*/
public $inverseOf;
/**
* Clones internal objects.
*/
......@@ -106,7 +107,6 @@ trait ActiveRelationTrait
if ($callable !== null) {
call_user_func($callable, $relation);
}
return $this;
}
......@@ -133,7 +133,6 @@ trait ActiveRelationTrait
public function inverseOf($relationName)
{
$this->inverseOf = $relationName;
return $this;
}
......@@ -240,8 +239,24 @@ trait ActiveRelationTrait
$link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
foreach ($primaryModels as $i => $primaryModel) {
$key = $this->getModelKey($primaryModel, $link);
$value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
if ($this->multiple && count($link) == 1 && is_array($keys = $primaryModel->{reset($link)})) {
$value = [];
foreach ($keys as $key) {
if (isset($buckets[$key])) {
if ($this->indexBy !== null) {
// if indexBy is set, array_merge will cause renumbering of numeric array
foreach($buckets[$key] as $bucketKey => $bucketValue) {
$value[$bucketKey] = $bucketValue;
}
} else {
$value = array_merge($value, $buckets[$key]);
}
}
}
} else {
$key = $this->getModelKey($primaryModel, $link);
$value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null);
}
if ($primaryModel instanceof ActiveRecordInterface) {
$primaryModel->populateRelation($name, $value);
} else {
......@@ -414,7 +429,11 @@ trait ActiveRelationTrait
$attribute = reset($this->link);
foreach ($models as $model) {
if (($value = $model[$attribute]) !== null) {
$values[] = $value;
if (is_array($value)) {
$values = array_merge($values, $value);
} else {
$values[] = $value;
}
}
}
} else {
......
......@@ -1282,8 +1282,16 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
$delete ? $model->delete() : $model->save(false);
} elseif ($p1) {
foreach ($relation->link as $b) {
$this->$b = null;
foreach ($relation->link as $a => $b) {
if (is_array($this->$b)) { // relation via array valued attribute
if (($key = array_search($model->$a, $this->$b, false)) !== false) {
$values = $this->$b;
unset($values[$key]);
$this->$b = $values;
}
} else {
$this->$b = null;
}
}
$delete ? $this->delete() : $this->save(false);
} else {
......@@ -1354,16 +1362,22 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} else {
/* @var $relatedModel ActiveRecordInterface */
$relatedModel = $relation->modelClass;
$nulls = [];
$condition = [];
foreach ($relation->link as $a => $b) {
$nulls[$a] = null;
$condition[$a] = $this->$b;
}
if ($delete) {
$relatedModel::deleteAll($condition);
if (!$delete && count($relation->link) == 1 && is_array($this->{$b = reset($relation->link)})) {
// relation via array valued attribute
$this->$b = [];
$this->save(false);
} else {
$relatedModel::updateAll($nulls, $condition);
$nulls = [];
$condition = [];
foreach ($relation->link as $a => $b) {
$nulls[$a] = null;
$condition[$a] = $this->$b;
}
if ($delete) {
$relatedModel::deleteAll($condition);
} else {
$relatedModel::updateAll($nulls, $condition);
}
}
}
......@@ -1383,7 +1397,11 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
if ($value === null) {
throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is null.');
}
$foreignModel->$fk = $value;
if (is_array($foreignModel->$fk)) { // relation via array valued attribute
$foreignModel->$fk = array_merge($foreignModel->$fk, [$value]);
} else {
$foreignModel->$fk = $value;
}
}
$foreignModel->save(false);
}
......
......@@ -45,6 +45,12 @@ class Order extends ActiveRecord
})->orderBy('item.id');
}
public function getItemsIndexed()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->indexBy('id');
}
public function getItemsWithNullFK()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
......@@ -21,7 +21,7 @@ class Order extends ActiveRecord
public function attributes()
{
return ['id', 'customer_id', 'created_at', 'total'];
return ['id', 'customer_id', 'created_at', 'total', 'itemsArray'];
}
public function getCustomer()
......@@ -34,12 +34,26 @@ class Order extends ActiveRecord
return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
}
/**
* A relation to Item defined via array valued attribute
*/
public function getItemsByArrayValue()
{
return $this->hasMany(Item::className(), ['id' => 'itemsArray'])->indexBy('id');
}
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->orderBy('id');
}
public function getItemsIndexed()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->indexBy('id');
}
public function getItemsWithNullFK()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
......@@ -27,6 +27,12 @@ class Order extends ActiveRecord
});
}
public function getItemsIndexed()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->indexBy('id');
}
public function getItemsInOrder1()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
......@@ -121,15 +121,15 @@ class ActiveRecordTest extends ElasticSearchTestCase
$order = new Order();
$order->id = 1;
$order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false);
$order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0, 'itemsArray' => [1, 2]], false);
$order->save(false);
$order = new Order();
$order->id = 2;
$order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false);
$order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0, 'itemsArray' => [4, 5, 3]], false);
$order->save(false);
$order = new Order();
$order->id = 3;
$order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false);
$order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0, 'itemsArray' => [2]], false);
$order->save(false);
$orderItem = new OrderItem();
......@@ -696,6 +696,134 @@ class ActiveRecordTest extends ElasticSearchTestCase
$this->assertEquals(0, count($orderItems));
}
public function testArrayAttributes()
{
$this->assertTrue(is_array(Order::findOne(1)->itemsArray));
$this->assertTrue(is_array(Order::findOne(2)->itemsArray));
$this->assertTrue(is_array(Order::findOne(3)->itemsArray));
}
public function testArrayAttributeRelationLazy()
{
$order = Order::findOne(1);
$items = $order->itemsByArrayValue;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$this->assertTrue($items[1] instanceof Item);
$this->assertTrue($items[2] instanceof Item);
$order = Order::findOne(2);
$items = $order->itemsByArrayValue;
$this->assertEquals(3, count($items));
$this->assertTrue(isset($items[3]));
$this->assertTrue(isset($items[4]));
$this->assertTrue(isset($items[5]));
$this->assertTrue($items[3] instanceof Item);
$this->assertTrue($items[4] instanceof Item);
$this->assertTrue($items[5] instanceof Item);
}
public function testArrayAttributeRelationEager()
{
/* @var $order Order */
$order = Order::find()->with('itemsByArrayValue')->where(['id' => 1])->one();
$this->assertTrue($order->isRelationPopulated('itemsByArrayValue'));
$items = $order->itemsByArrayValue;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$this->assertTrue($items[1] instanceof Item);
$this->assertTrue($items[2] instanceof Item);
/* @var $order Order */
$order = Order::find()->with('itemsByArrayValue')->where(['id' => 2])->one();
$this->assertTrue($order->isRelationPopulated('itemsByArrayValue'));
$items = $order->itemsByArrayValue;
$this->assertEquals(3, count($items));
$this->assertTrue(isset($items[3]));
$this->assertTrue(isset($items[4]));
$this->assertTrue(isset($items[5]));
$this->assertTrue($items[3] instanceof Item);
$this->assertTrue($items[4] instanceof Item);
$this->assertTrue($items[5] instanceof Item);
}
public function testArrayAttributeRelationLink()
{
/* @var $order Order */
$order = Order::find()->where(['id' => 1])->one();
$items = $order->itemsByArrayValue;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$item = Item::get(5);
$order->link('itemsByArrayValue', $item);
$this->afterSave();
$items = $order->itemsByArrayValue;
$this->assertEquals(3, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$this->assertTrue(isset($items[5]));
// check also after refresh
$this->assertTrue($order->refresh());
$items = $order->itemsByArrayValue;
$this->assertEquals(3, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$this->assertTrue(isset($items[5]));
}
public function testArrayAttributeRelationUnLink()
{
/* @var $order Order */
$order = Order::find()->where(['id' => 1])->one();
$items = $order->itemsByArrayValue;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$item = Item::get(2);
$order->unlink('itemsByArrayValue', $item);
$this->afterSave();
$items = $order->itemsByArrayValue;
$this->assertEquals(1, count($items));
$this->assertTrue(isset($items[1]));
$this->assertFalse(isset($items[2]));
// check also after refresh
$this->assertTrue($order->refresh());
$items = $order->itemsByArrayValue;
$this->assertEquals(1, count($items));
$this->assertTrue(isset($items[1]));
$this->assertFalse(isset($items[2]));
}
public function testArrayAttributeRelationUnLinkAll()
{
/* @var $order Order */
$order = Order::find()->where(['id' => 1])->one();
$items = $order->itemsByArrayValue;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
$order->unlinkAll('itemsByArrayValue');
$this->afterSave();
$items = $order->itemsByArrayValue;
$this->assertEquals(0, count($items));
// check also after refresh
$this->assertTrue($order->refresh());
$items = $order->itemsByArrayValue;
$this->assertEquals(0, count($items));
}
// TODO test AR with not mapped PK
}
......@@ -6,6 +6,7 @@ use yiiunit\data\ar\sphinx\ActiveRecord;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
use yiiunit\data\ar\sphinx\ArticleIndex;
use yiiunit\data\ar\sphinx\ArticleDb;
use yiiunit\data\ar\sphinx\TagDb;
/**
* @group sphinx
......@@ -34,11 +35,14 @@ class ExternalActiveRelationTest extends SphinxTestCase
$this->assertEquals(1, count($article->relatedRecords));
// has many :
/*$this->assertFalse($article->isRelationPopulated('tags'));
$this->assertFalse($article->isRelationPopulated('tags'));
$tags = $article->tags;
$this->assertTrue($article->isRelationPopulated('tags'));
$this->assertEquals(3, count($tags));
$this->assertTrue($tags[0] instanceof TagDb);*/
$this->assertEquals(count($article->tag), count($tags));
$this->assertTrue($tags[0] instanceof TagDb);
foreach ($tags as $tag) {
$this->assertTrue(in_array($tag->id, $article->tag));
}
}
public function testFindEager()
......@@ -52,10 +56,20 @@ class ExternalActiveRelationTest extends SphinxTestCase
$this->assertTrue($articles[1]->source instanceof ArticleDb);
// has many :
/*$articles = ArticleIndex::find()->with('tags')->all();
$articles = ArticleIndex::find()->with('tags')->all();
$this->assertEquals(2, count($articles));
$this->assertTrue($articles[0]->isRelationPopulated('tags'));
$this->assertTrue($articles[1]->isRelationPopulated('tags'));*/
$this->assertTrue($articles[1]->isRelationPopulated('tags'));
foreach ($articles as $article) {
$this->assertTrue($article->isRelationPopulated('tags'));
$tags = $article->tags;
$this->assertEquals(count($article->tag), count($tags));
//var_dump($tags);
$this->assertTrue($tags[0] instanceof TagDb);
foreach ($tags as $tag) {
$this->assertTrue(in_array($tag->id, $article->tag));
}
}
}
/**
......
......@@ -1064,4 +1064,29 @@ trait ActiveRecordTestTrait
$customers = $customerClass::find()->where(['IN', 'id', []])->all();
$this->assertEquals(0, count($customers));
}
public function testFindEagerIndexBy()
{
/* @var $this TestCase|ActiveRecordTestTrait */
/* @var $orderClass \yii\db\ActiveRecordInterface */
$orderClass = $this->getOrderClass();
/* @var $order Order */
$order = $orderClass::find()->with('itemsIndexed')->where(['id' => 1])->one();
$this->assertTrue($order->isRelationPopulated('itemsIndexed'));
$items = $order->itemsIndexed;
$this->assertEquals(2, count($items));
$this->assertTrue(isset($items[1]));
$this->assertTrue(isset($items[2]));
/* @var $order Order */
$order = $orderClass::find()->with('itemsIndexed')->where(['id' => 2])->one();
$this->assertTrue($order->isRelationPopulated('itemsIndexed'));
$items = $order->itemsIndexed;
$this->assertEquals(3, count($items));
$this->assertTrue(isset($items[3]));
$this->assertTrue(isset($items[4]));
$this->assertTrue(isset($items[5]));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment