Commit 177ab042 authored by Jens Segers's avatar Jens Segers

Tweaking embedsMany, check issue #138

parent 828a490d
...@@ -359,6 +359,43 @@ The belongsToMany relation will not use a pivot "table", but will push id's to a ...@@ -359,6 +359,43 @@ The belongsToMany relation will not use a pivot "table", but will push id's to a
Other relations are not yet supported, but may be added in the future. Read more about these relations on http://four.laravel.com/docs/eloquent#relationships Other relations are not yet supported, but may be added in the future. Read more about these relations on http://four.laravel.com/docs/eloquent#relationships
### EmbedsMany Relations
If you want to embed documents, rather than referencing them, you can use the `embedsMany` relation:
use Jenssegers\Mongodb\Model as Eloquent;
class User extends Eloquent {
public function books()
{
return $this->embedsMany('Book');
}
}
Now we can access the post's comments through the dynamic property:
$books = User::first()->books;
Inserting and updating embedded documents works just like the `belongsTo` relation:
$book = new Book(array('title' => 'A Game of Thrones'));
$user = User::first();
$book = $user->books()->save($book);
You can remove an embedded document by using the `destroy()` method:
$book = $user->books()->first();
$user->books()->destroy($book->_id);
Again, you may override the conventional local key by passing a second argument to the embedsMany method:
return $this->embedsMany('Book', 'local_key');
### MySQL Relations ### MySQL Relations
If you're using a hybrid MongoDB and SQL setup, you're in luck! The model will automatically return a MongoDB- or SQL-relation based on the type of the related model. Of course, if you want this functionality to work both ways, your SQL-models will need to extend `Jenssegers\Eloquent\Model`. Note that this functionality only works for hasOne, hasMany and belongsTo relations. If you're using a hybrid MongoDB and SQL setup, you're in luck! The model will automatically return a MongoDB- or SQL-relation based on the type of the related model. Of course, if you want this functionality to work both ways, your SQL-models will need to extend `Jenssegers\Eloquent\Model`. Note that this functionality only works for hasOne, hasMany and belongsTo relations.
......
<?php namespace Jenssegers\Eloquent; <?php namespace Jenssegers\Eloquent;
use LogicException;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\MorphOne;
...@@ -9,7 +8,6 @@ use Illuminate\Database\Eloquent\Relations\Relation; ...@@ -9,7 +8,6 @@ use Illuminate\Database\Eloquent\Relations\Relation;
use Jenssegers\Mongodb\Relations\BelongsTo; use Jenssegers\Mongodb\Relations\BelongsTo;
use Jenssegers\Mongodb\Relations\BelongsToMany; use Jenssegers\Mongodb\Relations\BelongsToMany;
use Jenssegers\Mongodb\Relations\EmbedsMany; use Jenssegers\Mongodb\Relations\EmbedsMany;
use Jenssegers\Mongodb\Relations\EmbeddedRelation;
use Jenssegers\Mongodb\Query\Builder as QueryBuilder; use Jenssegers\Mongodb\Query\Builder as QueryBuilder;
abstract class Model extends \Illuminate\Database\Eloquent\Model { abstract class Model extends \Illuminate\Database\Eloquent\Model {
...@@ -230,36 +228,31 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model { ...@@ -230,36 +228,31 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model {
* @param string $collection * @param string $collection
* @return \Illuminate\Database\Eloquent\Relations\EmbedsMany * @return \Illuminate\Database\Eloquent\Relations\EmbedsMany
*/ */
protected function embedsMany($related, $collection = null) protected function embedsMany($related, $localKey = null, $foreignKey = null, $relation = null)
{ {
// If no collection name was provided, we assume that the standard is the if (is_null($localKey))
// related model camel_cased, pluralized and suffixed with '_ids'
if (is_null($collection))
{ {
$collection = str_plural(snake_case($related)) . '_ids'; $localKey = snake_case(str_plural($related)) . '_ids';
} }
return new EmbedsMany($this, $related, $collection); if (is_null($foreignKey))
{
$foreignKey = snake_case(class_basename($this));
} }
/** // If no relation name was given, we will use this debug backtrace to extract
* Get a relationship value from a method. // the calling method's name and use that as the relationship name as most
* // of the time this will be what we desire to use for the relatinoships.
* @param string $key if (is_null($relation))
* @param string $camelKey
* @return mixed
*/
protected function getRelationshipFromMethod($key, $camelKey)
{ {
$relations = $this->$camelKey(); list(, $caller) = debug_backtrace(false);
if ( ! $relations instanceof Relation and ! $relations instanceof EmbeddedRelation) $relation = $caller['function'];
{
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation or Jenssegers\Mongodb\Relations\EmbeddedRelation');
} }
return $this->relations[$key] = $relations->getResults(); $query = $this->newQuery();
return new EmbedsMany($query, $this, $localKey, $foreignKey, $relation);
} }
/** /**
......
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Model;
abstract class EmbeddedRelation {
/**
* The parent model instance.
*
* @var \Illuminate\Database\Eloquent\Model
*/
protected $parent;
/**
* The related model class name.
*
* @var string
*/
protected $related;
/**
* Create a new has many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $related
* @param string $collection
* @return void
*/
public function __construct(Model $parent, $related)
{
$this->parent = $parent;
$this->related = $related;
}
/**
* Get the results of the relationship.
*
* @return mixed
*/
abstract public function getResults();
}
<?php namespace Jenssegers\Mongodb\Relations; <?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use MongoId; use MongoId;
class EmbedsMany extends EmbeddedRelation { class EmbedsMany extends Relation {
/** /**
* The parent collection attribute where the related are stored. * The local key of the parent model.
* *
* @var string * @var string
*/ */
protected $collection; protected $localKey;
/** /**
* Create a new has many relationship instance. * The foreign key of the parent model.
* *
* @var string
*/
protected $foreignKey;
/**
* The "name" of the relationship.
*
* @var string
*/
protected $relation;
/**
* Create a new embeds many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent * @param \Illuminate\Database\Eloquent\Model $parent
* @param string $related * @param string $localKey
* @param string $collection * @param string $relation
* @return void * @return void
*/ */
public function __construct(Model $parent, $related, $collection) public function __construct(Builder $query, Model $parent, $localKey, $foreignKey, $relation)
{ {
$this->collection = $collection; $this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->relation = $relation;
parent::__construct($parent, $related); parent::__construct($query, $parent);
} }
/** /**
* Get the results of the relationship. * Set the base constraints on the relation query.
* *
* @return Illuminate\Database\Eloquent\Collection * @return void
*/ */
public function getResults() public function addConstraints()
{ {
$models = new Collection(); }
$modelsAttributes = $this->parent->getAttribute($this->collection); /**
* Set the constraints for an eager load of the relation.
*
* @param array $models
* @return void
*/
public function addEagerConstraints(array $models)
{
}
if (is_array($modelsAttributes)) /**
* Initialize the relation on a set of models.
*
* @param array $models
* @param string $relation
* @return void
*/
public function initRelation(array $models, $relation)
{ {
foreach ($modelsAttributes as $attributes) foreach ($models as $model)
{ {
$models->push(new $this->related($attributes)); $model->setRelation($relation, $this->related->newCollection());
} }
return $models;
} }
/**
* 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)
{
return $models; return $models;
} }
/** /**
* Create a new instance and attach it to the parent model. * Get the results of the relationship.
* *
* @param array $attributes * @return Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Model
*/ */
public function build(array $attributes) public function getResults()
{ {
if ( ! isset($attributes['_id'])) $attributes['_id'] = new MongoId; // Get embedded documents.
$results = $this->getEmbedded();
$collection = $this->parent->getAttribute($this->collection); $models = array();
$collection[''.$attributes['_id']] = $attributes;
$this->parent->setAttribute($this->collection, $collection);
return new $this->related($attributes); // Wrap documents in model objects.
foreach ($results as $result)
{
$model = $this->related->newFromBuilder($result);
// Attatch the parent relation to the embedded model.
$model->setRelation($this->foreignKey, $this->parent);
$models[] = $model;
}
return $this->related->newCollection($models);
} }
/** /**
* Attach an instance to the parent model. * Shorthand to get the results of the relationship.
* *
* @param \Illuminate\Database\Eloquent\Model $model * @return Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Model
*/ */
public function add($model) public function get()
{ {
return $this->build($model->toArray()); return $this->getResults();
} }
/** /**
* Create a new instance, attach it to the parent model and save this model. * Attach a model instance to the parent model.
* *
* @param array $attributes * @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model * @return \Illuminate\Database\Eloquent\Model
*/ */
public function create(array $attributes) public function save(Model $model)
{ {
$instance = $this->build($attributes); // Insert a new document.
if (!$model->exists)
$this->parent->save(); {
return $this->performInsert($model);
}
return $instance; // Update an existing document.
else
{
return $this->performUpdate($model);
}
} }
/** /**
* Attach a model instance to the parent model and save this model. * Perform a model insert operation.
* *
* @param \Illuminate\Database\Eloquent\Model $model * @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model * @return \Illuminate\Database\Eloquent\Model
*/ */
public function push($model) protected function performInsert(Model $model)
{ {
return $this->create($model->toArray()); // Create a new key.
if (!$model->getKey())
{
$model->setAttribute($model->getKeyName(), new MongoId);
}
// Set timestamps.
if ($model->usesTimestamps())
{
$time = $model->freshTimestamp();
$model->setUpdatedAt($time);
$model->setCreatedAt($time);
}
$model->exists = true;
// Get existing embedded documents.
$documents = $this->getEmbedded();
$documents[] = $model->getAttributes();
$this->setEmbedded($documents);
return $this->parent->save() ? $model : false;
} }
/** /**
* Create an array of new instances and attach them to the parent model. * Perform a model update operation.
* *
* @param array|Traversable $modelsAttributes * @param \Illuminate\Database\Eloquent\Model $model
* @return array * @return bool
*/ */
public function buildMany($modelsAttributes) protected function performUpdate(Model $model)
{ {
$instances = array(); // Update timestamps.
if ($model->usesTimestamps())
{
$time = $model->freshTimestamp();
$model->setUpdatedAt($time);
}
// Get existing embedded documents.
$documents = $this->getEmbedded();
$key = $model->getKey();
$primaryKey = $model->getKeyName();
foreach ($modelsAttributes as $attributes) // Update timestamps.
if ($model->usesTimestamps())
{ {
$instances[] = $this->build($attributes); $time = $model->freshTimestamp();
$model->setUpdatedAt($time);
} }
return $instances; // Replace the document
foreach ($documents as $i => $document)
{
if ($document[$primaryKey] == $key)
{
$documents[$i] = $model->getAttributes();
break;
}
}
$this->setEmbedded($documents);
return $this->parent->save() ? $model : false;
} }
/** /**
* Attach an array of new instances to the parent model. * Attach an array of models to the parent instance.
* *
* @param array|Traversable $models * @param array $models
* @return array * @return array
*/ */
public function addMany($models) public function saveMany(array $models)
{ {
$modelsAttributes = $this->getAttributesOf($models); array_walk($models, array($this, 'save'));
return $this->buildMany($modelsAttributes); return $models;
} }
/** /**
* Create an array of new instances, attach them to the parent model and save this model. * Create a new instance of the related model.
* *
* @param array|Traversable $modelsAttributes * @param array $attributes
* @return \Illuminate\Database\Eloquent\Model
*/
public function create(array $attributes)
{
// Here we will set the raw attributes to avoid hitting the "fill" method so
// that we do not have to worry about a mass accessor rules blocking sets
// on the models. Otherwise, some of these attributes will not get set.
$instance = $this->related->newInstance();
$instance->setRawAttributes($attributes);
return $this->save($instance);
}
/**
* Create an array of new instances of the related model.
*
* @param array $records
* @return array * @return array
*/ */
public function createMany($modelsAttributes) public function createMany(array $records)
{ {
$instances = $this->buildMany($modelsAttributes); $instances = array();
$this->parent->save(); foreach ($records as $record)
{
$instances[] = $this->create($record);
}
return $instances; return $instances;
} }
/** /**
* Attach an array of new instances to the parent model and save this model. * Destroy the embedded models for the given IDs.
* *
* @param array|Traversable $models * @param array|int $ids
* @return array * @return int
*/ */
public function pushMany($models) public function destroy($ids)
{ {
$modelsAttributes = $this->getAttributesOf($models); // We'll initialize a count here so we will return the total number of deletes
// for the operation. The developers can then check this number as a boolean
// type value or get this total count of records deleted for logging, etc.
$count = 0;
$ids = is_array($ids) ? $ids : func_get_args();
// Get existing embedded documents.
$documents = $this->getEmbedded();
return $this->createMany($modelsAttributes); $primaryKey = $this->related->getKeyName();
foreach ($documents as $i => $document)
{
if (in_array($document[$primaryKey], $ids))
{
unset($documents[$i]);
$count++;
}
}
$this->setEmbedded($documents);
$this->parent->save();
return $count;
} }
/** /**
* Transform a list of models to a list of models' attributes * Get the embedded documents array
* *
* @param array|Traversable $models
* @return array * @return array
*/ */
protected function getAttributesOf($models) public function getEmbedded()
{ {
$modelsAttributes = array(); // Get raw attributes to skip relations and accessors.
$attributes = $this->parent->getAttributes();
foreach ($models as $key => $model) return isset($attributes[$this->localKey]) ? $attributes[$this->localKey] : array();
{
$modelsAttributes[$key] = $model->getAttributes();
} }
return $modelsAttributes; /**
* Set the embedded documents array
*
* @param array $models
*/
public function setEmbedded(array $models)
{
$attributes = $this->parent->getAttributes();
$attributes[$this->localKey] = $models;
// Set raw attributes to skip mutators.
$this->parent->setRawAttributes($attributes);
// Set the relation on the parent.
$this->parent->setRelation($this->relation, $this->getResults());
} }
} }
...@@ -269,7 +269,8 @@ class RelationsTest extends PHPUnit_Framework_TestCase { ...@@ -269,7 +269,8 @@ class RelationsTest extends PHPUnit_Framework_TestCase {
$client = Client::create(array('name' => 'Jane Doe')); $client = Client::create(array('name' => 'Jane Doe'));
$photo = Photo::create(array('url' => 'http://graph.facebook.com/john.doe/picture')); $photo = Photo::create(array('url' => 'http://graph.facebook.com/john.doe/picture'));
$user->photos()->save($photo); $photo = $user->photos()->save($photo);
$this->assertEquals(1, $user->photos->count()); $this->assertEquals(1, $user->photos->count());
$this->assertEquals($photo->id, $user->photos->first()->id); $this->assertEquals($photo->id, $user->photos->first()->id);
...@@ -282,49 +283,64 @@ class RelationsTest extends PHPUnit_Framework_TestCase { ...@@ -282,49 +283,64 @@ class RelationsTest extends PHPUnit_Framework_TestCase {
$this->assertEquals($photo->imageable->name, $user->name); $this->assertEquals($photo->imageable->name, $user->name);
} }
public function testEmbedsManySaveOneRelated() public function testEmbedsManySave()
{ {
$user = User::create(array('name' => 'John Doe')); $user = User::create(array('name' => 'John Doe'));
$address = new Address(array('city' => 'London'));
$user->addresses()->create(array('city' => 'Paris')); $address = $user->addresses()->save($address);
$user->addresses()->push(new Address(array('city' => 'London'))); $this->assertEquals(array('London'), $user->addresses->lists('city'));
$user->addresses()->build(array('city' => 'Bruxelles')); $this->assertInstanceOf('DateTime', $address->created_at);
$user->addresses()->add(new Address(array('city' => 'New-York'))); $this->assertInstanceOf('DateTime', $address->updated_at);
$freshUser = User::find($user->id); $address = $user->addresses()->save(new Address(array('city' => 'Paris')));
$this->assertEquals(array('Paris', 'London', 'Bruxelles', 'New-York'), $user->addresses->lists('city'));
$this->assertEquals(array('Paris', 'London'), $freshUser->addresses->lists('city'));
$user->save(); $freshUser = User::find($user->_id);
$freshUser = User::find($user->id); $this->assertEquals(array('London', 'Paris'), $freshUser->addresses->lists('city'));
$this->assertEquals(array('Paris', 'London', 'Bruxelles', 'New-York'), $freshUser->addresses->lists('city'));
$address->city = 'New York';
$freshUser->addresses()->save($address);
$this->assertEquals(2, count($freshUser->addresses));
$this->assertEquals(2, count($freshUser->addresses()->get()));
$this->assertEquals(2, $freshUser->addresses->count());
$this->assertEquals(array('London', 'New York'), $freshUser->addresses->lists('city'));
$address = $freshUser->addresses->first();
$this->assertEquals('London', $address->city);
$this->assertInstanceOf('DateTime', $address->created_at);
$this->assertInstanceOf('DateTime', $address->updated_at);
$this->assertInstanceOf('User', $address->user);
} }
public function testEmbedsManySaveManyRelated() public function testEmbedsManySaveMany()
{ {
$user = User::create(array('name' => 'John Doe')); $user = User::create(array('name' => 'John Doe'));
$user->addresses()->saveMany(array(new Address(array('city' => 'London')), new Address(array('city' => 'Bristol'))));
$user->addresses()->createMany(array(array('city' => 'Paris'), array('city' => 'Rouen'))); $this->assertEquals(array('London', 'Bristol'), $user->addresses->lists('city'));
$user->addresses()->pushMany(array(new Address(array('city' => 'London')), new Address(array('city' => 'Bristol'))));
$user->addresses()->buildMany(array(array('city' => 'Bruxelles')));
$user->addresses()->addMany(new Collection(array(new Address(array('city' => 'New-York')))));
$freshUser = User::find($user->id); $freshUser = User::find($user->id);
$this->assertEquals(array('Paris', 'Rouen', 'London', 'Bristol', 'Bruxelles', 'New-York'), $user->addresses->lists('city')); $this->assertEquals(array('London', 'Bristol'), $freshUser->addresses->lists('city'));
$this->assertEquals(array('Paris', 'Rouen', 'London', 'Bristol'), $freshUser->addresses->lists('city')); }
$user->save(); public function testEmbedsManyCreate()
$freshUser = User::find($user->id); {
$this->assertEquals(array('Paris', 'Rouen', 'London', 'Bristol', 'Bruxelles', 'New-York'), $freshUser->addresses->lists('city')); $user = new User(array('name' => 'John Doe'));
$user->addresses()->create(array('city' => 'Bruxelles'));
$this->assertEquals(array('Bruxelles'), $user->addresses->lists('city'));
$freshUser = User::find($user->id);
$this->assertEquals(array('Bruxelles'), $freshUser->addresses->lists('city'));
} }
public function testEmbedsManyCreateId() public function testEmbedsManyDestroy()
{ {
$user = new User(array('name' => 'John Doe')); $user = User::create(array('name' => 'John Doe'));
$user->addresses()->build(array('city' => 'Bruxelles')); $user->addresses()->saveMany(array(new Address(array('city' => 'London')), new Address(array('city' => 'Bristol')), new Address(array('city' => 'Bruxelles'))));
$this->assertInstanceOf('MongoId', $user->addresses->first()->_id);
$address = $user->addresses->first();
$user->addresses()->destroy($address->_id);
$this->assertEquals(array('Bristol', 'Bruxelles'), $user->addresses->lists('city'));
} }
} }
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