From 43d8b40575f6fb034bfdcf69d106c2d9078dba92 Mon Sep 17 00:00:00 2001 From: Simon Karlen Date: Thu, 8 Jul 2021 15:45:36 +0200 Subject: [PATCH] changed relation handling --- src/ActiveQuery.php | 166 ++++++++++++++++++++++--------------- src/ActiveRecord.php | 23 ++++- tests/RelationTest.php | 22 ++++- tests/models/RestModel.php | 10 +++ 4 files changed, 152 insertions(+), 69 deletions(-) diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index 1e2946c..9b8b640 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -56,30 +56,6 @@ public function createCommand($db = null) return parent::createCommand($db); } - /** - * {@inheritdoc} - */ - public function all($db = null) - { - return parent::all($db); - } - - /** - * {@inheritdoc} - * @throws InvalidConfigException - */ - public function one($db = null) - { - $row = parent::one($db); - if ($row !== false) { - $models = $this->populate(isset($row[0]) ? $row : [$row]); - - return reset($models) ?: null; - } - - return null; - } - /** * {@inheritdoc} * @throws InvalidConfigException @@ -134,46 +110,6 @@ public function prepare($builder) return $query; } - /** - * Joins with the specified relations. - * - * This method allows you to reuse existing relation definitions to perform JOIN queries. - * Based on the definition of the specified relation(s), the method will append one or multiple - * JOIN statements to the current query. - * - * @param string|array $with the relations to be joined. This can either be a string, representing a relation name or - * an array with the following semantics: - * - * - Each array element represents a single relation. - * - You may specify the relation name as the array key and provide an anonymous functions that - * can be used to modify the relation queries on-the-fly as the array value. - * - If a relation query does not need modification, you may use the relation name as the array value. - * - * Sub-relations can also be specified, see [[with()]] for the syntax. - * - * In the following you find some examples: - * - * ```php - * // find all orders that contain books, and eager loading "books" - * Order::find()->joinWith('books')->all(); - * // find all orders, eager loading "books", and sort the orders and books by the book names. - * Order::find()->joinWith([ - * 'books' => function (\simialbi\yii2\rest\ActiveQuery $query) { - * $query->orderBy('item.name'); - * } - * ])->all(); - * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table - * Order::find()->joinWith(['books b'])->where(['b.category' => 'Science fiction'])->all(); - * ``` - * - * @return $this the query object itself - */ - public function joinWith($with) - { - $this->joinWith[] = (array)$with; - return $this; - } - /** * Builds join with clauses */ @@ -196,7 +132,6 @@ private function buildJoinWith() } } - /** * Modifies the current query by adding join fragments based on the given relations. * @param ActiveRecord $model the primary model @@ -224,6 +159,7 @@ protected function joinWithRelations($model, $with) } } } + /** * Joins a parent query with a child query. * The current query object will be modified accordingly. @@ -240,6 +176,31 @@ private function joinWithRelation($parent, $child) } } + /** + * {@inheritdoc} + */ + public function all($db = null) + { + return parent::all($db); + } + + /** + * {@inheritdoc} + * @throws InvalidConfigException + */ + public function one($db = null) + { + $row = parent::one($db); + if ($row !== false) { + $models = $this->populate(isset($row[0]) ? $row : [$row]); + + return reset($models) ?: null; + } + + return null; + } + + /** * {@inheritdoc} * @throws InvalidConfigException @@ -268,14 +229,46 @@ public function populate($rows) return $models; } + /** + * {@inheritDoc} + */ + protected function createModels($rows) + { + if ($this->asArray) { + return $rows; + } else { + $models = []; + /* @var $class ActiveRecord */ + $class = $this->modelClass; + foreach ($rows as $row) { + $model = $class::instantiate($row); + /** @var $modelClass ActiveRecord */ + $modelClass = get_class($model); + $modelClass::populateRecord($model, $row); + if (!empty($this->join)) { + foreach ($this->join as $join) { + if (isset($join[1], $row[$join[1]])) { + $relation = $model->getRelation($join[1]); + $rows = (ArrayHelper::isAssociative($row[$join[1]])) ? [$row[$join[1]]] : $row[$join[1]]; + $relations = $relation->populate($rows); + $model->populateRelation($join[1], $relation->multiple ? $relations : $relations[0]); + } + } + } + $models[] = $model; + } + return $models; + } + } + /** * Removes duplicated models by checking their primary key values. * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. * * @param array $models the models to be checked * - * @throws InvalidConfigException if model primary key is empty * @return array the distinctive models + * @throws InvalidConfigException if model primary key is empty */ private function removeDuplicatedModels($models) { @@ -303,6 +296,7 @@ private function removeDuplicatedModels($models) } } } elseif (empty($pks)) { + /** @var $class string */ throw new InvalidConfigException("Primary key of '{$class}' can not be empty."); } else { // single column primary key @@ -323,4 +317,44 @@ private function removeDuplicatedModels($models) return array_values($models); } + + /** + * Joins with the specified relations. + * + * This method allows you to reuse existing relation definitions to perform JOIN queries. + * Based on the definition of the specified relation(s), the method will append one or multiple + * JOIN statements to the current query. + * + * @param string|array $with the relations to be joined. This can either be a string, representing a relation name or + * an array with the following semantics: + * + * - Each array element represents a single relation. + * - You may specify the relation name as the array key and provide an anonymous functions that + * can be used to modify the relation queries on-the-fly as the array value. + * - If a relation query does not need modification, you may use the relation name as the array value. + * + * Sub-relations can also be specified, see [[with()]] for the syntax. + * + * In the following you find some examples: + * + * ```php + * // find all orders that contain books, and eager loading "books" + * Order::find()->joinWith('books')->all(); + * // find all orders, eager loading "books", and sort the orders and books by the book names. + * Order::find()->joinWith([ + * 'books' => function (\simialbi\yii2\rest\ActiveQuery $query) { + * $query->orderBy('item.name'); + * } + * ])->all(); + * // find all orders that contain books of the category 'Science fiction', using the alias "b" for the books table + * Order::find()->joinWith(['books b'])->where(['b.category' => 'Science fiction'])->all(); + * ``` + * + * @return $this the query object itself + */ + public function joinWith($with) + { + $this->joinWith[] = (array)$with; + return $this; + } } diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 205153a..cc72064 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -32,7 +32,7 @@ class ActiveRecord extends BaseActiveRecord public function attributes() { if (empty($this->_attributeFields)) { - $regex = '#^@property(?:-(read|write))?(?:(?:\s+)([^\s]+))?(?:\s+)\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)#'; + $regex = '#^@property(?:-(read|write))?(?:\s+([^\s]+))?\s+\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)#'; $typeRegex = '#^(bool(ean)?|int(eger)?|float|double|string|array)$#'; $reflection = new \ReflectionClass($this); $docLines = preg_split('~\R~u', $reflection->getDocComment()); @@ -53,10 +53,11 @@ public function attributes() /** * {@inheritdoc} + * @throws InvalidConfigException */ public static function primaryKey() { - new InvalidConfigException('The primaryKey() method of RestClient ActiveRecord has to be implemented by child classes.'); + throw new InvalidConfigException('The primaryKey() method of RestClient ActiveRecord has to be implemented by child classes.'); } /** @@ -222,4 +223,22 @@ public function unlinkAll($name, $delete = false) { throw new NotSupportedException('unlinkAll() is not supported by RestClient, use unlink() instead.'); } + + /** + * {@inheritDoc} + * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface + */ + public function hasOne($class, $link) + { + return parent::hasOne($class, $link); + } + + /** + * {@inheritDoc} + * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface + */ + public function hasMany($class, $link) + { + return parent::hasMany($class, $link); + } } diff --git a/tests/RelationTest.php b/tests/RelationTest.php index 602a01e..2f53fe5 100644 --- a/tests/RelationTest.php +++ b/tests/RelationTest.php @@ -22,7 +22,7 @@ protected function setUp() Yii::$app->log->logger->flush(); } - public function testRelationGet() + public function testRelationGetAll() { $fixture = new RestModelFixture(); $fixture->load(); @@ -42,4 +42,24 @@ public function testRelationGet() $this->assertEquals('GET', $logEntry['method']); $this->assertStringStartsWith('https://api.site.com/related-rest-models?filter%5Brest_model_id%5D=1', $logEntry['url']); } + + public function testRelationGetOne() + { + $fixture = new RestModelFixture(); + $fixture->load(); + + /* @var $model RestModel */ + $model = $fixture->getModel(0); + + $this->assertInstanceOf(RestModel::class, $model); + + Yii::$app->log->logger->flush(); + + $related = $model->relatedRest; + + $logEntry = $this->parseLogs(); + + $this->assertEquals('GET', $logEntry['method']); + $this->assertStringStartsWith('https://api.site.com/related-rest-models?filter%5Brest_model_id%5D=1', $logEntry['url']); + } } diff --git a/tests/models/RestModel.php b/tests/models/RestModel.php index 7eab7e9..d8c66ff 100644 --- a/tests/models/RestModel.php +++ b/tests/models/RestModel.php @@ -23,6 +23,7 @@ * @property string $updated_by * * @property-read RelatedRestModel[] $relatedRests + * @property-read RelatedRestModel $relatedRest */ class RestModel extends ActiveRecord { @@ -42,4 +43,13 @@ public function getRelatedRests() { return $this->hasMany(RelatedRestModel::class, ['rest_model_id' => 'id']); } + + /** + * Get related rest + * @return \simialbi\yii2\rest\ActiveQuery|\yii\db\ActiveQuery|\yii\db\ActiveQueryInterface + */ + public function getRelatedRest() + { + return $this->hasOne(RelatedRestModel::class, ['rest_model_id' => 'id']); + } }