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 ...@@ -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 #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 #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 #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 #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 #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) - 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 ...@@ -62,6 +62,7 @@ trait ActiveRelationTrait
*/ */
public $inverseOf; public $inverseOf;
/** /**
* Clones internal objects. * Clones internal objects.
*/ */
...@@ -106,7 +107,6 @@ trait ActiveRelationTrait ...@@ -106,7 +107,6 @@ trait ActiveRelationTrait
if ($callable !== null) { if ($callable !== null) {
call_user_func($callable, $relation); call_user_func($callable, $relation);
} }
return $this; return $this;
} }
...@@ -133,7 +133,6 @@ trait ActiveRelationTrait ...@@ -133,7 +133,6 @@ trait ActiveRelationTrait
public function inverseOf($relationName) public function inverseOf($relationName)
{ {
$this->inverseOf = $relationName; $this->inverseOf = $relationName;
return $this; return $this;
} }
...@@ -240,8 +239,24 @@ trait ActiveRelationTrait ...@@ -240,8 +239,24 @@ trait ActiveRelationTrait
$link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link);
foreach ($primaryModels as $i => $primaryModel) { foreach ($primaryModels as $i => $primaryModel) {
$key = $this->getModelKey($primaryModel, $link); if ($this->multiple && count($link) == 1 && is_array($keys = $primaryModel->{reset($link)})) {
$value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? [] : null); $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) { if ($primaryModel instanceof ActiveRecordInterface) {
$primaryModel->populateRelation($name, $value); $primaryModel->populateRelation($name, $value);
} else { } else {
...@@ -414,7 +429,11 @@ trait ActiveRelationTrait ...@@ -414,7 +429,11 @@ trait ActiveRelationTrait
$attribute = reset($this->link); $attribute = reset($this->link);
foreach ($models as $model) { foreach ($models as $model) {
if (($value = $model[$attribute]) !== null) { if (($value = $model[$attribute]) !== null) {
$values[] = $value; if (is_array($value)) {
$values = array_merge($values, $value);
} else {
$values[] = $value;
}
} }
} }
} else { } else {
......
...@@ -1282,8 +1282,16 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface ...@@ -1282,8 +1282,16 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} }
$delete ? $model->delete() : $model->save(false); $delete ? $model->delete() : $model->save(false);
} elseif ($p1) { } elseif ($p1) {
foreach ($relation->link as $b) { foreach ($relation->link as $a => $b) {
$this->$b = null; 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); $delete ? $this->delete() : $this->save(false);
} else { } else {
...@@ -1354,16 +1362,22 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface ...@@ -1354,16 +1362,22 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
} else { } else {
/* @var $relatedModel ActiveRecordInterface */ /* @var $relatedModel ActiveRecordInterface */
$relatedModel = $relation->modelClass; $relatedModel = $relation->modelClass;
$nulls = []; if (!$delete && count($relation->link) == 1 && is_array($this->{$b = reset($relation->link)})) {
$condition = []; // relation via array valued attribute
foreach ($relation->link as $a => $b) { $this->$b = [];
$nulls[$a] = null; $this->save(false);
$condition[$a] = $this->$b;
}
if ($delete) {
$relatedModel::deleteAll($condition);
} else { } 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 ...@@ -1383,7 +1397,11 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
if ($value === null) { if ($value === null) {
throw new InvalidCallException('Unable to link models: the primary key of ' . get_class($primaryModel) . ' is 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); $foreignModel->save(false);
} }
......
...@@ -45,6 +45,12 @@ class Order extends ActiveRecord ...@@ -45,6 +45,12 @@ class Order extends ActiveRecord
})->orderBy('item.id'); })->orderBy('item.id');
} }
public function getItemsIndexed()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->indexBy('id');
}
public function getItemsWithNullFK() public function getItemsWithNullFK()
{ {
return $this->hasMany(Item::className(), ['id' => 'item_id']) return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
...@@ -21,7 +21,7 @@ class Order extends ActiveRecord ...@@ -21,7 +21,7 @@ class Order extends ActiveRecord
public function attributes() public function attributes()
{ {
return ['id', 'customer_id', 'created_at', 'total']; return ['id', 'customer_id', 'created_at', 'total', 'itemsArray'];
} }
public function getCustomer() public function getCustomer()
...@@ -34,12 +34,26 @@ class Order extends ActiveRecord ...@@ -34,12 +34,26 @@ class Order extends ActiveRecord
return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); 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() public function getItems()
{ {
return $this->hasMany(Item::className(), ['id' => 'item_id']) return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->orderBy('id'); ->via('orderItems')->orderBy('id');
} }
public function getItemsIndexed()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->indexBy('id');
}
public function getItemsWithNullFK() public function getItemsWithNullFK()
{ {
return $this->hasMany(Item::className(), ['id' => 'item_id']) return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
...@@ -27,6 +27,12 @@ class Order extends ActiveRecord ...@@ -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() public function getItemsInOrder1()
{ {
return $this->hasMany(Item::className(), ['id' => 'item_id']) return $this->hasMany(Item::className(), ['id' => 'item_id'])
......
...@@ -121,15 +121,15 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -121,15 +121,15 @@ class ActiveRecordTest extends ElasticSearchTestCase
$order = new Order(); $order = new Order();
$order->id = 1; $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->save(false);
$order = new Order(); $order = new Order();
$order->id = 2; $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->save(false);
$order = new Order(); $order = new Order();
$order->id = 3; $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); $order->save(false);
$orderItem = new OrderItem(); $orderItem = new OrderItem();
...@@ -696,6 +696,134 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -696,6 +696,134 @@ class ActiveRecordTest extends ElasticSearchTestCase
$this->assertEquals(0, count($orderItems)); $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 // TODO test AR with not mapped PK
} }
...@@ -6,6 +6,7 @@ use yiiunit\data\ar\sphinx\ActiveRecord; ...@@ -6,6 +6,7 @@ use yiiunit\data\ar\sphinx\ActiveRecord;
use yiiunit\data\ar\ActiveRecord as ActiveRecordDb; use yiiunit\data\ar\ActiveRecord as ActiveRecordDb;
use yiiunit\data\ar\sphinx\ArticleIndex; use yiiunit\data\ar\sphinx\ArticleIndex;
use yiiunit\data\ar\sphinx\ArticleDb; use yiiunit\data\ar\sphinx\ArticleDb;
use yiiunit\data\ar\sphinx\TagDb;
/** /**
* @group sphinx * @group sphinx
...@@ -34,11 +35,14 @@ class ExternalActiveRelationTest extends SphinxTestCase ...@@ -34,11 +35,14 @@ class ExternalActiveRelationTest extends SphinxTestCase
$this->assertEquals(1, count($article->relatedRecords)); $this->assertEquals(1, count($article->relatedRecords));
// has many : // has many :
/*$this->assertFalse($article->isRelationPopulated('tags')); $this->assertFalse($article->isRelationPopulated('tags'));
$tags = $article->tags; $tags = $article->tags;
$this->assertTrue($article->isRelationPopulated('tags')); $this->assertTrue($article->isRelationPopulated('tags'));
$this->assertEquals(3, count($tags)); $this->assertEquals(count($article->tag), count($tags));
$this->assertTrue($tags[0] instanceof TagDb);*/ $this->assertTrue($tags[0] instanceof TagDb);
foreach ($tags as $tag) {
$this->assertTrue(in_array($tag->id, $article->tag));
}
} }
public function testFindEager() public function testFindEager()
...@@ -52,10 +56,20 @@ class ExternalActiveRelationTest extends SphinxTestCase ...@@ -52,10 +56,20 @@ class ExternalActiveRelationTest extends SphinxTestCase
$this->assertTrue($articles[1]->source instanceof ArticleDb); $this->assertTrue($articles[1]->source instanceof ArticleDb);
// has many : // has many :
/*$articles = ArticleIndex::find()->with('tags')->all(); $articles = ArticleIndex::find()->with('tags')->all();
$this->assertEquals(2, count($articles)); $this->assertEquals(2, count($articles));
$this->assertTrue($articles[0]->isRelationPopulated('tags')); $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 ...@@ -1064,4 +1064,29 @@ trait ActiveRecordTestTrait
$customers = $customerClass::find()->where(['IN', 'id', []])->all(); $customers = $customerClass::find()->where(['IN', 'id', []])->all();
$this->assertEquals(0, count($customers)); $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