From 34e95e90fa5a62aad885da902c09690b61144da5 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 15:26:27 +0100 Subject: [PATCH 01/11] Get and set nested attributes --- .project | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .project diff --git a/.project b/.project new file mode 100644 index 000000000..bf89462f1 --- /dev/null +++ b/.project @@ -0,0 +1,11 @@ + + + Laravel-MongoDB + + + + + + + + From ddfb1ba5ee136c6fcabfb8de628e91c422f293ab Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 15:32:23 +0100 Subject: [PATCH 02/11] Get and set nested attributes --- src/Jenssegers/Mongodb/Model.php | 132 ++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/src/Jenssegers/Mongodb/Model.php b/src/Jenssegers/Mongodb/Model.php index bbefcc472..621c45119 100644 --- a/src/Jenssegers/Mongodb/Model.php +++ b/src/Jenssegers/Mongodb/Model.php @@ -232,7 +232,137 @@ public function dropColumn($columns) // Perform unset only on current document return $query = $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns); } - + + /** + * Set a given attribute on the model. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function setAttribute($key, $value) + { + // Set the nested key to studly case + $nestedKey = studly_case(str_replace('.', '_', $key)); + + // First we will check for the presence of a mutator for the set operation + // which simply lets the developers tweak the attribute as it is set on + // the model, such as "json_encoding" an listing of data for storage. + if ($this->hasSetMutator($nestedKey)) + { + $method = 'set'.$nestedKey.'Attribute'; + + return $this->{$method}($value); + } + + // If an attribute is listed as a "date", we'll convert it from a DateTime + // instance into a form proper for storage on the database tables using + // the connection grammar's date format. We will auto set the values. + elseif (in_array($nestedKey, $this->getDates())) + { + if ($value) + { + $value = $this->fromDateTime($value); + } + } + + array_set($this->attributes, $key, $value); + } + + /** + * Get an attribute from the model. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + // Get the key as studly case + $nestedKey = studly_case(str_replace('.', '_', $key)); + + // Check if the nested key exists in the document + $inAttributes = array_get($this->attributes, $key, false); + + // If the key references an attribute, we can just go ahead and return the + // plain attribute value from the model. This allows every attribute to + // be dynamically accessed through the _get method without accessors. + if ( $inAttributes or $this->hasGetMutator($nestedKey)) + { + return $this->getAttributeValue($key); + } + + // If the key already exists in the relationships array, it just means the + // relationship has already been loaded, so we'll just return it out of + // here because there is no need to query within the relations twice. + $relationship = array_get($this->relations, $key, false); + + if ( $relationship ) + { + return $relationship; + } + + // If the "attribute" exists as a method on the model, we will just assume + // it is a relationship and will load and return results from the query + // and hydrate the relationship's value on the "relationships" array. + $camelKey = camel_case($key); + + if (method_exists($this, $camelKey)) + { + $relations = $this->$camelKey()->getResults(); + + array_set($this->relations, $key, $relations); + + return $relations; + } + } + + /** + * Get a plain attribute (not a relationship). + * + * @param string $key + * @return mixed + */ + protected function getAttributeValue($key) + { + $value = $this->getAttributeFromArray($key); + + $snakeKey = str_replace('.', '_', $key); + + // If the attribute has a get mutator, we will call that then return what + // it returns as the value, which is useful for transforming values on + // retrieval from the model to a form that is more useful for usage. + if ($this->hasGetMutator($snakeKey)) + { + return $this->mutateAttribute($snakeKey, $value); + } + + // If the attribute is listed as a date, we will convert it to a DateTime + // instance on retrieval, which makes it quite convenient to work with + // date fields without having to create a mutator for each property. + elseif (in_array($snakeKey, $this->getDates())) + { + if ($value) return $this->asDateTime($value); + } + + return $value; + } + + /** + * Get an attribute from the $attributes array. + * + * @param string $key + * @return mixed + */ + protected function getAttributeFromArray($key) + { + $nestedValue = array_get($this->attributes, $key, false); + + if ( $nestedValue ) + { + return $nestedValue; + } + } + /** * Handle dynamic method calls into the method. * From 10fb54bd90736ed318f7bac0c643cbf5a847f5ed Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 17:52:22 +0100 Subject: [PATCH 03/11] Added mutators for tests --- src/Jenssegers/Mongodb/Model.php | 38 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Jenssegers/Mongodb/Model.php b/src/Jenssegers/Mongodb/Model.php index 621c45119..47ffd3c1e 100644 --- a/src/Jenssegers/Mongodb/Model.php +++ b/src/Jenssegers/Mongodb/Model.php @@ -243,14 +243,14 @@ public function dropColumn($columns) public function setAttribute($key, $value) { // Set the nested key to studly case - $nestedKey = studly_case(str_replace('.', '_', $key)); + $studlyKey = studly_case(str_replace('.', '_', $key)); // First we will check for the presence of a mutator for the set operation // which simply lets the developers tweak the attribute as it is set on // the model, such as "json_encoding" an listing of data for storage. - if ($this->hasSetMutator($nestedKey)) + if ($this->hasSetMutator($studlyKey)) { - $method = 'set'.$nestedKey.'Attribute'; + $method = 'set'.$studlyKey.'Attribute'; return $this->{$method}($value); } @@ -258,7 +258,7 @@ public function setAttribute($key, $value) // If an attribute is listed as a "date", we'll convert it from a DateTime // instance into a form proper for storage on the database tables using // the connection grammar's date format. We will auto set the values. - elseif (in_array($nestedKey, $this->getDates())) + elseif (in_array($studlyKey, $this->getDates())) { if ($value) { @@ -266,6 +266,13 @@ public function setAttribute($key, $value) } } + // If key is in dot notation, leave it as it is. If it is in camel case + // convert it to dot notation to be inserted into the document + if ( ! strstr($key, '.') && ! strstr($key, '_') && preg_match('/[A-Z]/', $key)) + { + $key = str_replace('_', '.', snake_case($key)); + } + array_set($this->attributes, $key, $value); } @@ -277,19 +284,27 @@ public function setAttribute($key, $value) */ public function getAttribute($key) { - // Get the key as studly case - $nestedKey = studly_case(str_replace('.', '_', $key)); + // Get the key as studly case if getAttribute() has been called + $studlyKey = strstr($key, '.') ? studly_case(str_replace('.', '_', $key)) : $key; - // Check if the nested key exists in the document + // If attribute was requested by a getter check if it is in a nested array + $inNestedAttributes = array_get($this->attributes, str_replace('_', '.', snake_case($key)), false); + + // Check if the nested value exists in the document $inAttributes = array_get($this->attributes, $key, false); - + // If the key references an attribute, we can just go ahead and return the // plain attribute value from the model. This allows every attribute to // be dynamically accessed through the _get method without accessors. - if ( $inAttributes or $this->hasGetMutator($nestedKey)) + if ( $inAttributes or $this->hasGetMutator($key) ) { return $this->getAttributeValue($key); } + + if ( $inNestedAttributes ) + { + return $this->getAttributeValue(str_replace('_', '.', snake_case($key))); + } // If the key already exists in the relationships array, it just means the // relationship has already been loaded, so we'll just return it out of @@ -355,6 +370,11 @@ protected function getAttributeValue($key) */ protected function getAttributeFromArray($key) { + if ( ! strstr($key, '.') && ! strstr($key, '_')) + { + $key = str_replace('_', '.', snake_case($key)); + } + $nestedValue = array_get($this->attributes, $key, false); if ( $nestedValue ) From 2a3bc6cde048344634dcd8d5c3c5c95d27d7f2c6 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 17:52:52 +0100 Subject: [PATCH 04/11] Added mutators for tests --- tests/models/User.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/models/User.php b/tests/models/User.php index 022df5427..06f8bb147 100644 --- a/tests/models/User.php +++ b/tests/models/User.php @@ -25,6 +25,22 @@ public function role() { return $this->hasOne('Role'); } + + /** + * Mutator for nested properties + */ + public function getNotesNote3Attribute($value) + { + return $value . 'mutated'; + } + + /** + * Mutator for nested properties + */ + public function setNotesNote3Attribute($value) + { + $this->attributes['notes']['note3'] = strtolower($value); + } /** * Get the unique identifier for the user. From 0dc92249edef848ff62b48139a14e6d78ec413d7 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 17:53:14 +0100 Subject: [PATCH 05/11] Created test cases for nested getters and setters --- tests/ModelTest.php | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 505023d8a..46e67a3b8 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -303,5 +303,38 @@ public function testUnset() $this->assertFalse(isset($user2->note1)); $this->assertFalse(isset($user2->note2)); } - + + public function testSetAndGetNestedValues() + { + $user = User::create(array('name' => 'John Doe', 'notes' => array('note1' => 'ABC', 'note2' => 'DEF'))); + + $user = User::find($user->_id); + + $this->assertTrue($user->getAttribute('notes.note1') === 'ABC'); + $user->setAttribute('notes.note1', 'XYZ'); + + $this->assertFalse($user->getAttribute('notes.note1') === 'ABC'); + $this->assertTrue($user->getAttribute('notes.note1') === 'XYZ'); + + $user->notesNote4 = "GHI"; + $this->assertTrue($user->notesNote4 === "GHI"); + + } + + public function testSetAndGetNestedMutators() + { + $user = User::create(array('name' => 'John Doe', 'notes' => array('note1' => 'ABC', 'note3' => 'DEF'))); + + $user = User::find($user->_id); + + $note3 = $user->notesNote3; + + $this->assertTrue($note3 === 'DEFmutated'); + $this->assertTrue($user->notesNote1 === 'ABC'); + + $user->notesNote3 = "ABCDEF"; + + $this->assertFalse($user->notesNote3 === 'ABCDEF'); + $this->assertTrue($user->notesNote3 === 'abcdefmutated'); + } } From 8c5efa5e1d8c06d43998589281b7811d6c2676ef Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 17:54:05 +0100 Subject: [PATCH 06/11] removed project --- .project | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .project diff --git a/.project b/.project deleted file mode 100644 index bf89462f1..000000000 --- a/.project +++ /dev/null @@ -1,11 +0,0 @@ - - - Laravel-MongoDB - - - - - - - - From ff0f41a2aaa98acccf5960b4b07dc738d9774b80 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 10 Oct 2013 17:57:20 +0100 Subject: [PATCH 07/11] Made nestedAttributes conditional --- src/Jenssegers/Mongodb/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jenssegers/Mongodb/Model.php b/src/Jenssegers/Mongodb/Model.php index 47ffd3c1e..01aa6e58f 100644 --- a/src/Jenssegers/Mongodb/Model.php +++ b/src/Jenssegers/Mongodb/Model.php @@ -288,7 +288,7 @@ public function getAttribute($key) $studlyKey = strstr($key, '.') ? studly_case(str_replace('.', '_', $key)) : $key; // If attribute was requested by a getter check if it is in a nested array - $inNestedAttributes = array_get($this->attributes, str_replace('_', '.', snake_case($key)), false); + $inNestedAttributes = ! strstr($key, '.') ? array_get($this->attributes, str_replace('_', '.', snake_case($key)), false) : false; // Check if the nested value exists in the document $inAttributes = array_get($this->attributes, $key, false); From a816631fd273b68badead8cf7ac903751c440608 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 16 Oct 2013 18:25:22 +0100 Subject: [PATCH 08/11] updates --- src/Jenssegers/Mongodb/Model.php | 188 ++---- .../Mongodb/Relations/BelongsToMany.php | 567 ++++++++++++++++++ tests/ModelTest.php | 36 +- tests/RelationsTest.php | 28 +- tests/models/Client.php | 14 + tests/models/User.php | 15 +- 6 files changed, 645 insertions(+), 203 deletions(-) create mode 100644 src/Jenssegers/Mongodb/Relations/BelongsToMany.php create mode 100644 tests/models/Client.php diff --git a/src/Jenssegers/Mongodb/Model.php b/src/Jenssegers/Mongodb/Model.php index 01aa6e58f..a3bd99846 100644 --- a/src/Jenssegers/Mongodb/Model.php +++ b/src/Jenssegers/Mongodb/Model.php @@ -7,6 +7,7 @@ use Jenssegers\Mongodb\DatabaseManager as Resolver; use Jenssegers\Mongodb\Builder as QueryBuilder; use Jenssegers\Mongodb\Relations\BelongsTo; +use Jenssegers\Mongodb\Relations\BelongsToMany; use DateTime; use MongoId; @@ -175,6 +176,43 @@ public function belongsTo($related, $foreignKey = null) return new BelongsTo($query, $this, $foreignKey, $relation); } + + /** + * Define a many-to-many relationship. + * + * @param string $related + * @param string $table + * @param string $foreignKey + * @param string $otherKey + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function belongsToMany($related, $collection = null, $foreignKey = null, $otherKey = null) + { + $caller = $this->getBelongsToManyCaller(); + + // First, we'll need to determine the foreign key and "other key" for the + // relationship. Once we have determined the keys we'll make the query + // instances as well as the relationship instances we need for this. + $foreignKey = $foreignKey ?: $this->getForeignKey() . 's'; + + $instance = new $related; + + $otherKey = $otherKey ?: $instance->getForeignKey() . 's'; + // If no table name was provided, we can guess it by concatenating the two + // models using underscores in alphabetical order. The two model names + // are transformed to snake case from their default CamelCase also. + if (is_null($collection)) + { + $collection = snake_case(str_plural(class_basename($related))); + } + + // Now we're ready to create a new query builder for the related model and + // the relationship instances for the relation. The relations will set + // appropriate query constraint and entirely manages the hydrations. + $query = $instance->newQuery(); + + return new BelongsToMany($query, $this, $collection, $foreignKey, $otherKey, $caller['function']); + } /** * Get a new query builder instance for the connection. @@ -232,157 +270,7 @@ public function dropColumn($columns) // Perform unset only on current document return $query = $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns); } - - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function setAttribute($key, $value) - { - // Set the nested key to studly case - $studlyKey = studly_case(str_replace('.', '_', $key)); - - // First we will check for the presence of a mutator for the set operation - // which simply lets the developers tweak the attribute as it is set on - // the model, such as "json_encoding" an listing of data for storage. - if ($this->hasSetMutator($studlyKey)) - { - $method = 'set'.$studlyKey.'Attribute'; - - return $this->{$method}($value); - } - - // If an attribute is listed as a "date", we'll convert it from a DateTime - // instance into a form proper for storage on the database tables using - // the connection grammar's date format. We will auto set the values. - elseif (in_array($studlyKey, $this->getDates())) - { - if ($value) - { - $value = $this->fromDateTime($value); - } - } - - // If key is in dot notation, leave it as it is. If it is in camel case - // convert it to dot notation to be inserted into the document - if ( ! strstr($key, '.') && ! strstr($key, '_') && preg_match('/[A-Z]/', $key)) - { - $key = str_replace('_', '.', snake_case($key)); - } - - array_set($this->attributes, $key, $value); - } - - /** - * Get an attribute from the model. - * - * @param string $key - * @return mixed - */ - public function getAttribute($key) - { - // Get the key as studly case if getAttribute() has been called - $studlyKey = strstr($key, '.') ? studly_case(str_replace('.', '_', $key)) : $key; - - // If attribute was requested by a getter check if it is in a nested array - $inNestedAttributes = ! strstr($key, '.') ? array_get($this->attributes, str_replace('_', '.', snake_case($key)), false) : false; - - // Check if the nested value exists in the document - $inAttributes = array_get($this->attributes, $key, false); - - // If the key references an attribute, we can just go ahead and return the - // plain attribute value from the model. This allows every attribute to - // be dynamically accessed through the _get method without accessors. - if ( $inAttributes or $this->hasGetMutator($key) ) - { - return $this->getAttributeValue($key); - } - - if ( $inNestedAttributes ) - { - return $this->getAttributeValue(str_replace('_', '.', snake_case($key))); - } - - // If the key already exists in the relationships array, it just means the - // relationship has already been loaded, so we'll just return it out of - // here because there is no need to query within the relations twice. - $relationship = array_get($this->relations, $key, false); - - if ( $relationship ) - { - return $relationship; - } - // If the "attribute" exists as a method on the model, we will just assume - // it is a relationship and will load and return results from the query - // and hydrate the relationship's value on the "relationships" array. - $camelKey = camel_case($key); - - if (method_exists($this, $camelKey)) - { - $relations = $this->$camelKey()->getResults(); - - array_set($this->relations, $key, $relations); - - return $relations; - } - } - - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - * @return mixed - */ - protected function getAttributeValue($key) - { - $value = $this->getAttributeFromArray($key); - - $snakeKey = str_replace('.', '_', $key); - - // If the attribute has a get mutator, we will call that then return what - // it returns as the value, which is useful for transforming values on - // retrieval from the model to a form that is more useful for usage. - if ($this->hasGetMutator($snakeKey)) - { - return $this->mutateAttribute($snakeKey, $value); - } - - // If the attribute is listed as a date, we will convert it to a DateTime - // instance on retrieval, which makes it quite convenient to work with - // date fields without having to create a mutator for each property. - elseif (in_array($snakeKey, $this->getDates())) - { - if ($value) return $this->asDateTime($value); - } - - return $value; - } - - /** - * Get an attribute from the $attributes array. - * - * @param string $key - * @return mixed - */ - protected function getAttributeFromArray($key) - { - if ( ! strstr($key, '.') && ! strstr($key, '_')) - { - $key = str_replace('_', '.', snake_case($key)); - } - - $nestedValue = array_get($this->attributes, $key, false); - - if ( $nestedValue ) - { - return $nestedValue; - } - } - /** * Handle dynamic method calls into the method. * diff --git a/src/Jenssegers/Mongodb/Relations/BelongsToMany.php b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php new file mode 100644 index 000000000..71c5d619d --- /dev/null +++ b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php @@ -0,0 +1,567 @@ +get(); + } + + /** + * Execute the query and get the first result. + * + * @param array $columns + * @return mixed + */ + public function first($columns = array('*')) + { + $results = $this->take(1)->get($columns); + + return count($results) > 0 ? $results->first() : null; + } + + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = array('*')) + { + // First we'll add the proper select columns onto the query so it is run with + // the proper columns. Then, we will get the results and hydrate out pivot + // models with the result of those columns as a separate model relation. + $select = $this->getSelectColumns($columns); + + $models = $this->query->addSelect($select)->getModels(); + + // If we actually found models we will also eager load any relationships that + // have been specified as needing to be eager loaded. This will solve the + // n + 1 query problem for the developer and also increase performance. + if (count($models) > 0) + { + $models = $this->query->eagerLoadRelations($models); + } + + return $this->related->newCollection($models); + } + + /** + * Set the select clause for the relation query. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + protected function getSelectColumns(array $columns = array('*')) + { + return $columns; + } + + /** + * Get a paginator for the "select" statement. + * + * @param int $perPage + * @param array $columns + * @return \Illuminate\Pagination\Paginator + */ + public function paginate($perPage = null, $columns = array('*')) + { + $this->query->addSelect($this->getSelectColumns($columns)); + + // When paginating results, we need to add the pivot columns to the query and + // then hydrate into the pivot objects once the results have been gathered + // from the database since this isn't performed by the Eloquent builder. + $pager = $this->query->paginate($perPage, $columns); + + return $pager; + } + + + /** + * Set the base constraints on the relation query. + * + * @return void + */ + public function addConstraints() + { + if (static::$constraints) + { + // Make sure that the primary key of the parent + // is in the relationship array of keys + $this->query->whereIn($this->foreignKey, array($this->parent->getKey())); + } + } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + $this->query->whereIn($this->getForeignKey(), $this->getKeys($models)); + } + + /** + * Initialize the relation on a set of models. + * + * @param array $models + * @param string $relation + * @return void + */ + public function initRelation(array $models, $relation) + { + foreach ($models as $model) + { + $model->setRelation($relation, $this->related->newCollection()); + } + + return $models; + } + + /** + * Get all of the IDs for the related models. + * + * @return array + */ + public function getRelatedIds() + { + $related = $this->getRelated(); + + $fullKey = $related->getQualifiedKeyName(); + + return $this->getQuery()->select($fullKey)->lists($related->getKeyName()); + } + + /** + * Save a new model and attach it to the parent model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param array $joining + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model + */ + public function save(Model $model, array $joining = array(), $touch = true) + { + if (is_array($model->{$this->otherKey})) + { + $model->push($this->foreignKey, (array)$this->parent->getKey()); + } + else + { + $model->{$this->foreignKey} = (array)$this->parent->getKey(); + } + + $model->save(array('touch' => false)); + + $this->attach($model->getKey(), $joining, $touch); + + return $model; + } + + /** + * Save an array of new models and attach them to the parent model. + * + * @param array $models + * @param array $joinings + * @return array + */ + public function saveMany(array $models, array $joinings = array()) + { + foreach ($models as $key => $model) + { + $this->save($model, (array) array_get($joinings, $key), false); + } + + $this->touchIfTouching(); + + return $models; + } + + /** + * Create a new instance of the related model. + * + * @param array $attributes + * @param array $joining + * @param bool $touch + * @return \Illuminate\Database\Eloquent\Model + */ + public function create(array $attributes, array $joining = array(), $touch = true) + { + $instance = $this->related->newInstance($attributes); + + // We save the ID of the parent model to the foreign key array on the new + // model before we save the new instance + $instance->{$this->foreignKey} = array((string)$this->parent->getKey()); + + $instance->save(array('touch' => false)); + + // Attach to the parent instance + $this->attach($instance->_id, $attributes, $touch); + + return $instance; + } + + /** + * Sync the intermediate tables with a list of IDs. + * + * @param array $ids + * @param bool $detaching + * @return void + */ + public function sync(array $ids, $detaching = true) + { + // First we need to attach any of the associated models that are not currently + // in this joining table. We'll spin through the given IDs, checking to see + // if they exist in the array of current ones, and if not we will insert. + $current = $this->newPivotQuery()->lists($this->otherKey); + + $records = $this->formatSyncList($ids); + + $detach = array_diff($current, array_keys($records)); + + // Next, we will take the differences of the currents and given IDs and detach + // all of the entities that exist in the "current" array but are not in the + // the array of the IDs given to the method which will complete the sync. + if ($detaching and count($detach) > 0) + { + $this->detach($detach); + } + + // Now we are finally ready to attach the new records. Note that we'll disable + // touching until after the entire operation is complete so we don't fire a + // ton of touch operations until we are totally done syncing the records. + $this->attachNew($records, $current, false); + + $this->touchIfTouching(); + } + + /** + * Format the sync list so that it is keyed by ID. + * + * @param array $records + * @return array + */ + protected function formatSyncList(array $records) + { + $results = array(); + + foreach ($records as $id => $attributes) + { + if ( ! is_array($attributes)) + { + list($id, $attributes) = array($attributes, array()); + } + + $results[$id] = $attributes; + } + + return $results; + } + + /** + * Attach all of the IDs that aren't in the current array. + * + * @param array $records + * @param array $current + * @param bool $touch + * @return void + */ + protected function attachNew(array $records, array $current, $touch = true) + { + foreach ($records as $id => $attributes) + { + // If the ID is not in the list of existing pivot IDs, we will insert a new pivot + // record, otherwise, we will just update this existing record on this joining + // table, so that the developers will easily update these records pain free. + if ( ! in_array($id, $current)) + { + $this->attach($id, $attributes, $touch); + } + elseif (count($attributes) > 0) + { + $this->updateExistingPivot($id, $attributes, $touch); + } + } + } + + /** + * Attach a model to the parent. + * + * @param mixed $id + * @param array $attributes + * @param bool $touch + * @return void + */ + public function attach($id, array $attributes = array(), $touch = true) + { + if ($id instanceof Model) $id = $id->getKey(); + + $query = $this->newParentQuery(); + + $records = $this->createAttachRecords((array) $id, $attributes); + + $foreign = array_pluck($records, $this->otherKey)[0]; + + // Atteach to the parent of the relationship + $query->push($this->otherKey, $foreign) + ->update(array()); + } + + /** + * Create an array of records to insert into the pivot table. + * + * @param array $ids + * @return void + */ + protected function createAttachRecords($ids, array $attributes) + { + $records = array();; + + // To create the attachment records, we will simply spin through the IDs given + // and create a new record to insert for each ID. Each ID may actually be a + // key in the array, with extra attributes to be placed in other columns. + foreach ($ids as $key => $value) + { + $records[] = $this->attacher($key, $value, $attributes, false); + } + + return $records; + } + + /** + * Detach models from the relationship. + * + * @param int|array $ids + * @param bool $touch + * @return int + */ + public function detach($ids = array(), $touch = true) + { + if ($ids instanceof Model) $ids = (array) $ids->getKey(); + + $query = $this->newPivotQuery(); + + // If associated IDs were passed to the method we will only delete those + // associations, otherwise all of the association ties will be broken. + // We'll return the numbers of affected rows when we do the deletes. + $ids = (array) $ids; + + if (count($ids) > 0) + { + $query->whereIn($this->otherKey, $ids); + } + + if ($touch) $this->touchIfTouching(); + + // Once we have all of the conditions set on the statement, we are ready + // to run the delete on the pivot table. Then, if the touch parameter + // is true, we will go ahead and touch all related models to sync. + $results = $query->delete(); + + return $results; + } + + /** + * If we're touching the parent model, touch. + * + * @return void + */ + public function touchIfTouching() + { + if ($this->touchingParent()) $this->getParent()->touch(); + + if ($this->getParent()->touches($this->relationName)) $this->touch(); + } + + /** + * Determine if we should touch the parent on sync. + * + * @return bool + */ + protected function touchingParent() + { + return $this->getRelated()->touches($this->guessInverseRelation()); + } + + /** + * Attempt to guess the name of the inverse of the relation. + * + * @return string + */ + protected function guessInverseRelation() + { + return strtolower(str_plural(class_basename($this->getParent()))); + } + + /** + * Create a new query builder for the pivot table. + * + * @return \Illuminate\Database\Query\Builder + */ + protected function newPivotQuery() + { + $query = $this->newPivotStatement(); + + return $query->whereRaw(array($this->foreignKey => array('$in' => array($this->parent->getKey())))); + } + + /** + * Create a new query builder for the parent + * + * @return Jenssegers\Mongodb\Builder + */ + protected function newParentQuery() + { + $query = $this->parent->newQuery(); + + return $query->where($this->parent->getKeyName(), '=', $this->parent->getKey()); + } + + /** + * Match the eagerly loaded results to their parents. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + public function match(array $models, Collection $results, $relation) + { + $foreign = $this->otherKey; + + // First we will get to build a dictionary of the child models by their primary + // key of the relationship, then we can easily match the children back onto + // the parents using that dictionary and the primary key of the children. + $dictionary = array(); + + foreach ($results as $result) + { + $dictionary[$result->getKey()][] = $result; + } + + // Once we have the dictionary constructed, we can loop through all the parents + // and match back onto their children using these keys of the dictionary and + // the primary key of the children to map them onto the correct instances. + foreach ($models as $model) + { + if (is_array($model->$foreign)) + { + foreach ($model->$foreign as $relatedKey) + { + if (isset($dictionary[$relatedKey])) + { + $model->setRelation($relation, $dictionary[$relatedKey]); + } + } + } + } + + return $models; + } + + /** + * Build model dictionary keyed by the relations other key + */ + protected function buildDictionary(Collection $results) + { + $otherKey = $this->otherKey; + + $dictionary = array(); + + foreach ($results as $result) + { + $dictionary[$result]; + } + } + + /** + * Get the related model's updated at column name. + * + * @return string + */ + public function getRelatedFreshUpdate() + { + return array($this->related->getUpdatedAtColumn() => $this->related->freshTimestamp()); + } + + /** + * Get the fully qualified foreign key for the relation. + * + * @return string + */ + public function getForeignKey() + { + return $this->foreignKey; + } + + /** + * Get the fully qualified "other key" for the relation. + * + * @return string + */ + public function getOtherKey() + { + return $this->otherKey; + } +} \ No newline at end of file diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 46e67a3b8..70af9303e 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -303,38 +303,4 @@ public function testUnset() $this->assertFalse(isset($user2->note1)); $this->assertFalse(isset($user2->note2)); } - - public function testSetAndGetNestedValues() - { - $user = User::create(array('name' => 'John Doe', 'notes' => array('note1' => 'ABC', 'note2' => 'DEF'))); - - $user = User::find($user->_id); - - $this->assertTrue($user->getAttribute('notes.note1') === 'ABC'); - $user->setAttribute('notes.note1', 'XYZ'); - - $this->assertFalse($user->getAttribute('notes.note1') === 'ABC'); - $this->assertTrue($user->getAttribute('notes.note1') === 'XYZ'); - - $user->notesNote4 = "GHI"; - $this->assertTrue($user->notesNote4 === "GHI"); - - } - - public function testSetAndGetNestedMutators() - { - $user = User::create(array('name' => 'John Doe', 'notes' => array('note1' => 'ABC', 'note3' => 'DEF'))); - - $user = User::find($user->_id); - - $note3 = $user->notesNote3; - - $this->assertTrue($note3 === 'DEFmutated'); - $this->assertTrue($user->notesNote1 === 'ABC'); - - $user->notesNote3 = "ABCDEF"; - - $this->assertFalse($user->notesNote3 === 'ABCDEF'); - $this->assertTrue($user->notesNote3 === 'abcdefmutated'); - } -} +} \ No newline at end of file diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index 182efc0b8..bb4a5e199 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -3,16 +3,22 @@ class RelationsTest extends PHPUnit_Framework_TestCase { public function setUp() { - } - - public function tearDown() - { User::truncate(); Book::truncate(); Item::truncate(); Role::truncate(); + Client::truncate(); } + public function tearDown() + { + //User::truncate(); + //Book::truncate(); + //Item::truncate(); + //Role::truncate(); + //Client::truncate(); + } + public function testHasMany() { $author = User::create(array('name' => 'George R. R. Martin')); @@ -101,5 +107,17 @@ public function testWithHasOne() $this->assertInstanceOf('Role', $role); $this->assertEquals('admin', $role->type); } - + + public function testHasManyAndBelongsTo() + { + $user = User::create(array('name' => 'John Doe')); + $user2 = User::create(array('name' => 'Jane Doe')); + + $user->clients()->save(new Client(array('name' => 'Pork Pies Ltd.'))); + $user->clients()->create(array('name' => 'Buffet Bar Inc.')); + + $user = User::with('clients')->first(); + + dd($user); + } } diff --git a/tests/models/Client.php b/tests/models/Client.php new file mode 100644 index 000000000..de55ceab6 --- /dev/null +++ b/tests/models/Client.php @@ -0,0 +1,14 @@ +belongsToMany('User'); + } +} \ No newline at end of file diff --git a/tests/models/User.php b/tests/models/User.php index 06f8bb147..2e6862760 100644 --- a/tests/models/User.php +++ b/tests/models/User.php @@ -26,20 +26,9 @@ public function role() return $this->hasOne('Role'); } - /** - * Mutator for nested properties - */ - public function getNotesNote3Attribute($value) + public function clients() { - return $value . 'mutated'; - } - - /** - * Mutator for nested properties - */ - public function setNotesNote3Attribute($value) - { - $this->attributes['notes']['note3'] = strtolower($value); + return $this->belongsToMany('Client'); } /** From cac5933bea53ec0a1a311fe1cc2cc7bdd61ef6dd Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 17 Oct 2013 13:01:55 +0100 Subject: [PATCH 09/11] New belongsToMany --- .../Mongodb/Relations/BelongsToMany.php | 53 ++++--------------- tests/RelationsTest.php | 3 ++ 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/src/Jenssegers/Mongodb/Relations/BelongsToMany.php b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php index 71c5d619d..db7173fda 100644 --- a/src/Jenssegers/Mongodb/Relations/BelongsToMany.php +++ b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php @@ -478,61 +478,30 @@ protected function newParentQuery() return $query->where($this->parent->getKeyName(), '=', $this->parent->getKey()); } - /** - * Match the eagerly loaded results to their parents. + /** + * Build model dictionary keyed by the relation's foreign key. * - * @param array $models * @param \Illuminate\Database\Eloquent\Collection $results - * @param string $relation * @return array */ - public function match(array $models, Collection $results, $relation) + protected function buildDictionary(Collection $results) { - $foreign = $this->otherKey; + $foreign = $this->foreignKey; - // First we will get to build a dictionary of the child models by their primary - // key of the relationship, then we can easily match the children back onto - // the parents using that dictionary and the primary key of the children. + // First we will build a dictionary of child models keyed by the foreign key + // of the relation so that we will easily and quickly match them to their + // parents without having a possibly slow inner loops for every models. $dictionary = array(); foreach ($results as $result) { - $dictionary[$result->getKey()][] = $result; - } - - // Once we have the dictionary constructed, we can loop through all the parents - // and match back onto their children using these keys of the dictionary and - // the primary key of the children to map them onto the correct instances. - foreach ($models as $model) - { - if (is_array($model->$foreign)) + foreach ($result->$foreign as $single) { - foreach ($model->$foreign as $relatedKey) - { - if (isset($dictionary[$relatedKey])) - { - $model->setRelation($relation, $dictionary[$relatedKey]); - } - } + $dictionary[$single][] = $result; } } - - return $models; - } - - /** - * Build model dictionary keyed by the relations other key - */ - protected function buildDictionary(Collection $results) - { - $otherKey = $this->otherKey; - - $dictionary = array(); - - foreach ($results as $result) - { - $dictionary[$result]; - } + + return $dictionary; } /** diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index bb4a5e199..a1c9c7fc1 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -116,6 +116,9 @@ public function testHasManyAndBelongsTo() $user->clients()->save(new Client(array('name' => 'Pork Pies Ltd.'))); $user->clients()->create(array('name' => 'Buffet Bar Inc.')); + $user2->clients()->save(new Client(array('name' => 'GHD GmbH'))); + $user2->clients()->create(array('name' => 'Bayliss corp.')); + $user = User::with('clients')->first(); dd($user); From efc9d3a4434d3ce2f56e6f0dd74f5f4c95dadd47 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 20 Nov 2013 15:54:21 +0000 Subject: [PATCH 10/11] Relationship tests and many to many --- .../Mongodb/Relations/BelongsToMany.php | 200 +++--------------- tests/RelationsTest.php | 80 ++++++- 2 files changed, 108 insertions(+), 172 deletions(-) diff --git a/src/Jenssegers/Mongodb/Relations/BelongsToMany.php b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php index db7173fda..a0aff091e 100644 --- a/src/Jenssegers/Mongodb/Relations/BelongsToMany.php +++ b/src/Jenssegers/Mongodb/Relations/BelongsToMany.php @@ -5,80 +5,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentBelongsToMany; class BelongsToMany extends EloquentBelongsToMany { - - /** - * The intermediate table for the relation. - * - * @var string - */ - protected $table; - - /** - * The foreign key of the parent model. - * - * @var string - */ - protected $foreignKey; - - /** - * The associated key of the relation. - * - * @var string - */ - protected $otherKey; - - /** - * The "name" of the relationship. - * - * @var string - */ - protected $relationName; - - /** - * The pivot table columns to retrieve. - * - * @var array - */ - protected $pivotColumns = array(); - - /** - * Create a new has many relationship instance. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Model $parent - * @param string $table - * @param string $foreignKey - * @param string $otherKey - * @param string $relationName - * @return void - */ - public function __construct(Builder $query, Model $parent, $table, $foreignKey, $otherKey, $relationName = null) - { - parent::__construct($query, $parent, $table, $foreignKey, $otherKey, $relationName); - } - - /** - * Get the results of the relationship. - * - * @return mixed - */ - public function getResults() - { - return $this->get(); - } - - /** - * Execute the query and get the first result. - * - * @param array $columns - * @return mixed - */ - public function first($columns = array('*')) - { - $results = $this->take(1)->get($columns); - - return count($results) > 0 ? $results->first() : null; - } /** * Execute the query as a "select" statement. @@ -162,37 +88,6 @@ public function addEagerConstraints(array $models) $this->query->whereIn($this->getForeignKey(), $this->getKeys($models)); } - /** - * Initialize the relation on a set of models. - * - * @param array $models - * @param string $relation - * @return void - */ - public function initRelation(array $models, $relation) - { - foreach ($models as $model) - { - $model->setRelation($relation, $this->related->newCollection()); - } - - return $models; - } - - /** - * Get all of the IDs for the related models. - * - * @return array - */ - public function getRelatedIds() - { - $related = $this->getRelated(); - - $fullKey = $related->getQualifiedKeyName(); - - return $this->getQuery()->select($fullKey)->lists($related->getKeyName()); - } - /** * Save a new model and attach it to the parent model. * @@ -203,41 +98,13 @@ public function getRelatedIds() */ public function save(Model $model, array $joining = array(), $touch = true) { - if (is_array($model->{$this->otherKey})) - { - $model->push($this->foreignKey, (array)$this->parent->getKey()); - } - else - { - $model->{$this->foreignKey} = (array)$this->parent->getKey(); - } - $model->save(array('touch' => false)); $this->attach($model->getKey(), $joining, $touch); - + return $model; } - /** - * Save an array of new models and attach them to the parent model. - * - * @param array $models - * @param array $joinings - * @return array - */ - public function saveMany(array $models, array $joinings = array()) - { - foreach ($models as $key => $model) - { - $this->save($model, (array) array_get($joinings, $key), false); - } - - $this->touchIfTouching(); - - return $models; - } - /** * Create a new instance of the related model. * @@ -250,10 +117,7 @@ public function create(array $attributes, array $joining = array(), $touch = tru { $instance = $this->related->newInstance($attributes); - // We save the ID of the parent model to the foreign key array on the new - // model before we save the new instance - $instance->{$this->foreignKey} = array((string)$this->parent->getKey()); - + // Save the new instance before we attach it to other models $instance->save(array('touch' => false)); // Attach to the parent instance @@ -274,10 +138,14 @@ public function sync(array $ids, $detaching = true) // First we need to attach any of the associated models that are not currently // in this joining table. We'll spin through the given IDs, checking to see // if they exist in the array of current ones, and if not we will insert. - $current = $this->newPivotQuery()->lists($this->otherKey); + $current = $this->parent->{$this->otherKey}; + + // Check if the current array exists or not on the parent model and create it + // if it does not exist + if (is_null($current)) $current = array(); $records = $this->formatSyncList($ids); - + $detach = array_diff($current, array_keys($records)); // Next, we will take the differences of the currents and given IDs and detach @@ -338,10 +206,6 @@ protected function attachNew(array $records, array $current, $touch = true) { $this->attach($id, $attributes, $touch); } - elseif (count($attributes) > 0) - { - $this->updateExistingPivot($id, $attributes, $touch); - } } } @@ -357,15 +221,26 @@ public function attach($id, array $attributes = array(), $touch = true) { if ($id instanceof Model) $id = $id->getKey(); - $query = $this->newParentQuery(); + // Generate a new parent query instance + $parent = $this->newParentQuery(); + + // Generate a new related query instance + $related = $this->related->newInstance(); + // Set contraints on the related query + $related = $related->where($this->related->getKeyName(), $id); + $records = $this->createAttachRecords((array) $id, $attributes); - $foreign = array_pluck($records, $this->otherKey)[0]; + // Get the ID's to attach to the two documents + $otherIds = array_pluck($records, $this->otherKey); + $foreignIds = array_pluck($records, $this->foreignKey); - // Atteach to the parent of the relationship - $query->push($this->otherKey, $foreign) - ->update(array()); + // Attach to the parent model + $parent->push($this->otherKey, $otherIds[0])->update(array()); + + // Attach to the related model + $related->push($this->foreignKey, $foreignIds[0])->update(array()); } /** @@ -400,7 +275,7 @@ public function detach($ids = array(), $touch = true) { if ($ids instanceof Model) $ids = (array) $ids->getKey(); - $query = $this->newPivotQuery(); + $query = $this->newParentQuery(); // If associated IDs were passed to the method we will only delete those // associations, otherwise all of the association ties will be broken. @@ -417,9 +292,12 @@ public function detach($ids = array(), $touch = true) // Once we have all of the conditions set on the statement, we are ready // to run the delete on the pivot table. Then, if the touch parameter // is true, we will go ahead and touch all related models to sync. - $results = $query->delete(); - - return $results; + foreach($ids as $id) + { + $query->pull($this->otherKey, $id); + } + + return count($ids); } /** @@ -454,18 +332,6 @@ protected function guessInverseRelation() return strtolower(str_plural(class_basename($this->getParent()))); } - /** - * Create a new query builder for the pivot table. - * - * @return \Illuminate\Database\Query\Builder - */ - protected function newPivotQuery() - { - $query = $this->newPivotStatement(); - - return $query->whereRaw(array($this->foreignKey => array('$in' => array($this->parent->getKey())))); - } - /** * Create a new query builder for the parent * @@ -478,7 +344,7 @@ protected function newParentQuery() return $query->where($this->parent->getKeyName(), '=', $this->parent->getKey()); } - /** + /** * Build model dictionary keyed by the relation's foreign key. * * @param \Illuminate\Database\Eloquent\Collection $results @@ -495,7 +361,7 @@ protected function buildDictionary(Collection $results) foreach ($results as $result) { - foreach ($result->$foreign as $single) + foreach ($result->$foreign as $single) { $dictionary[$single][] = $result; } diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index a1c9c7fc1..149da26cf 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -111,16 +111,86 @@ public function testWithHasOne() public function testHasManyAndBelongsTo() { $user = User::create(array('name' => 'John Doe')); - $user2 = User::create(array('name' => 'Jane Doe')); $user->clients()->save(new Client(array('name' => 'Pork Pies Ltd.'))); $user->clients()->create(array('name' => 'Buffet Bar Inc.')); - $user2->clients()->save(new Client(array('name' => 'GHD GmbH'))); - $user2->clients()->create(array('name' => 'Bayliss corp.')); + $user = User::with('clients')->find($user->_id); - $user = User::with('clients')->first(); + $client = Client::with('users')->first(); - dd($user); + $clients = $client->getRelation('users'); + $users = $user->getRelation('clients'); + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $users); + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $clients); + $this->assertInstanceOf('Client', $users[0]); + $this->assertInstanceOf('User', $clients[0]); + $this->assertCount(2, $user->clients); + $this->assertCount(1, $client->users); + + // Now create a new user to an existing client + $client->users()->create(array('name' => 'Jane Doe')); + + $otherClient = User::where('name', '=', 'Jane Doe')->first()->clients()->get(); + + $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $otherClient); + $this->assertInstanceOf('Client', $otherClient[0]); + $this->assertCount(1, $otherClient); + + // Now attach an existing client to an existing user + $user = User::where('name', '=', 'Jane Doe')->first(); + $client = Client::Where('name', '=', 'Buffet Bar Inc.')->first(); + + // Check the models are what they should be + $this->assertInstanceOf('Client', $client); + $this->assertInstanceOf('User', $user); + + // Assert they are not attached + $this->assertFalse(in_array($client->_id, $user->client_ids)); + $this->assertFalse(in_array($user->_id, $client->user_ids)); + + // Attach the client to the user + $user->clients()->attach($client); + + // Get the new user model + $user = User::where('name', '=', 'Jane Doe')->first(); + $client = Client::Where('name', '=', 'Buffet Bar Inc.')->first(); + + // Assert they are attached + $this->assertTrue(in_array($client->_id, $user->client_ids)); + $this->assertTrue(in_array($user->_id, $client->user_ids)); + } + + public function testHasManyAndBelongsToAttachesExistingModels() + { + $user = User::create(array('name' => 'John Doe', 'client_ids' => array('1234523'))); + + $clients = array( + Client::create(array('name' => 'Pork Pies Ltd.'))->_id, + Client::create(array('name' => 'Buffet Bar Inc.'))->_id + ); + + $moreClients = array( + Client::create(array('name' => 'Boloni Ltd.'))->_id, + Client::create(array('name' => 'Meatballs Inc.'))->_id + ); + + $user->clients()->sync($clients); + + $user = User::with('clients')->find($user->_id); + + // Assert non attached ID's are detached succesfully + $this->assertFalse(in_array('1234523', $user->client_ids)); + + // Assert there are two client objects in the relationship + $this->assertCount(2, $user->clients); + + $user->clients()->sync($moreClients); + + $user = User::with('clients')->find($user->_id); + + // Assert there are now 4 client objects in the relationship + $this->assertCount(4, $user->clients); } } From 1544908fda554eba63a36053539bd03a0e489c98 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 20 Nov 2013 15:58:44 +0000 Subject: [PATCH 11/11] added teardown back in --- tests/RelationsTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index 149da26cf..b646014de 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -12,11 +12,11 @@ public function setUp() { public function tearDown() { - //User::truncate(); - //Book::truncate(); - //Item::truncate(); - //Role::truncate(); - //Client::truncate(); + User::truncate(); + Book::truncate(); + Item::truncate(); + Role::truncate(); + Client::truncate(); } public function testHasMany()