Commit b9e4cdae authored by Jens Segers's avatar Jens Segers

Tweaking relations and MongoId conversion

parent 9443e8b4
......@@ -169,27 +169,23 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
}
/**
* Set the array of model attributes. No checking is done.
* Get an attribute from the model.
*
* @param array $attributes
* @param bool $sync
* @return void
* @param string $key
* @return mixed
*/
public function setRawAttributes(array $attributes, $sync = false)
{
foreach($attributes as $key => &$value)
public function getAttribute($key)
{
/**
* MongoIds are converted to string to make it easier to pass
* the id to other instances or relations.
*/
if ($value instanceof MongoId)
$attribute = parent::getAttribute($key);
// If the attribute is a MongoId object, return it as a string.
// This is makes Eloquent relations a lot easier.
if ($attribute instanceof MongoId)
{
$value = (string) $value;
}
return (string) $attribute;
}
parent::setRawAttributes($attributes, $sync);
return $attribute;
}
/**
......@@ -223,7 +219,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
* @param mixed $columns
* @return int
*/
public function dropColumn($columns)
public function drop($columns)
{
if (!is_array($columns)) $columns = array($columns);
......@@ -234,7 +230,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
}
// Perform unset only on current document
return $query = $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
return $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
}
/**
......@@ -289,7 +285,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
// Unset method
if ($method == 'unset')
{
return call_user_func_array(array($this, 'dropColumn'), $parameters);
return call_user_func_array(array($this, 'drop'), $parameters);
}
return parent::__call($method, $parameters);
......
......@@ -59,7 +59,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
/**
* Execute a query for a single record by ID.
*
* @param int $id
* @param mixed $id
* @param array $columns
* @return mixed
*/
......@@ -377,8 +377,8 @@ class Builder extends \Illuminate\Database\Query\Builder {
$sequence = '_id';
}
// Return id as a string
return (string) $values[$sequence];
// Return id
return $values[$sequence];
}
}
......@@ -585,7 +585,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
* @param mixed $columns
* @return int
*/
public function dropColumn($columns)
public function drop($columns)
{
if (!is_array($columns)) $columns = array($columns);
......@@ -846,7 +846,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
{
if ($method == 'unset')
{
return call_user_func_array(array($this, 'dropColumn'), $parameters);
return call_user_func_array(array($this, 'drop'), $parameters);
}
return parent::__call($method, $parameters);
......
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Collection;
use Jenssegers\Mongodb\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentBelongsToMany;
class BelongsToMany extends EloquentBelongsToMany {
......@@ -36,12 +36,12 @@ class BelongsToMany extends EloquentBelongsToMany {
{
if (static::$constraints)
{
$this->query->where($this->foreignKey, $this->parent->getKey());
$this->query->where($this->getForeignKey(), '=', $this->parent->getKey());
}
}
/**
* Sync the intermediate tables with a list of IDs.
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param array $ids
* @param bool $detaching
......@@ -49,14 +49,12 @@ class BelongsToMany extends EloquentBelongsToMany {
*/
public function sync(array $ids, $detaching = true)
{
if ($ids instanceof Collection) $ids = $ids->modelKeys();
// 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->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();
$current = $this->parent->{$this->otherKey} ?: array();
$records = $this->formatSyncList($ids);
......@@ -133,27 +131,6 @@ class BelongsToMany extends EloquentBelongsToMany {
if ($touch) $this->touchIfTouching();
}
/**
* 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.
*
......@@ -165,6 +142,8 @@ class BelongsToMany extends EloquentBelongsToMany {
{
if ($ids instanceof Model) $ids = (array) $ids->getKey();
$query = $this->getNewRelatedQuery();
// 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.
......@@ -176,9 +155,6 @@ class BelongsToMany extends EloquentBelongsToMany {
$this->parent->pull($this->otherKey, $id);
}
// Get a new related query.
$query = $this->getNewRelatedQuery();
// Prepare the query to select all related objects.
if (count($ids) > 0)
{
......
......@@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use MongoId;
class EmbedsMany extends Relation {
......@@ -120,7 +119,7 @@ class EmbedsMany extends Relation {
*/
public function getResults()
{
return $this->toCollection($this->getEmbeddedRecords());
return $this->get();
}
/**
......@@ -130,27 +129,32 @@ class EmbedsMany extends Relation {
*/
public function get()
{
return $this->getResults();
return $this->toCollection($this->getEmbeddedRecords());
}
/**
* Get the results with given ids.
* Find an embedded model by its primary key.
*
* @param array $ids
* @param mixed $id
* @return \Illuminate\Database\Eloquent\Collection
*/
public function find(array $ids)
public function find($id)
{
$documents = $this->getEmbeddedRecords();
if ($id instanceof Model)
{
$id = $id->getKey();
}
$records = $this->getEmbeddedRecords();
$primaryKey = $this->related->getKeyName();
$documents = array_filter($documents, function ($document) use ($primaryKey, $ids)
$record = array_first($records, function($itemKey, $record) use ($primaryKey, $id)
{
return in_array($document[$primaryKey], $ids);
return $record[$primaryKey] == $id;
});
return $this->toCollection($documents);
return $record ? $this->toModel($record) : null;
}
/**
......@@ -165,13 +169,13 @@ class EmbedsMany extends Relation {
$this->updateTimestamps($model);
// Insert a new document.
// Attach a new model.
if ( ! $this->contains($model))
{
$result = $this->performInsert($model);
}
// Update an existing document.
// Update an existing model.
else
{
$result = $this->performUpdate($model);
......@@ -187,7 +191,7 @@ class EmbedsMany extends Relation {
}
/**
* Get the number of embedded documents.
* Get the number of embedded models.
*
* @return int
*/
......@@ -197,159 +201,32 @@ class EmbedsMany extends Relation {
}
/**
* Indicate if a model is already contained in the embedded documents
* Check if a model is already embedded.
*
* @param mixed $key
* @return bool
*/
public function contains($key)
{
if ($key instanceof Model)
{
$key = $key->getKey();
}
$primaryKey = $this->related->getKeyName();
foreach ($this->getEmbeddedRecords() as $record)
{
if ($record[$primaryKey] == $key) return true;
}
return false;
return ! is_null($this->find($key));
}
/**
* Attach a model instance to the parent model without persistence.
* Associate the model instance to the given parent, without saving it to the database.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
public function associate(Model $model)
{
// Insert the related model in the parent instance
if ( ! $this->contains($model))
{
return $this->associateNew($model);
}
// Update the related model in the parent instance
else
{
return $this->associateExisting($model);
}
}
/**
* Perform a model insert operation.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function performInsert(Model $model)
{
if ($this->fireModelEvent($model, 'creating') === false) return false;
// Insert the related model in the parent instance
$this->associateNew($model);
// Push the document to the database.
$result = $this->query->push($this->localKey, $model->getAttributes(), true);
if ($result)
{
$this->fireModelEvent($model, 'created', false);
return $model;
}
return false;
}
/**
* Perform a model update operation.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return Model|bool
*/
protected function performUpdate(Model $model)
{
if ($this->fireModelEvent($model, 'updating') === false) return false;
// Update the related model in the parent instance
$this->associateExisting($model);
// Get the correct foreign key value.
$id = $this->getForeignKeyValue($model->getKey());
// Update document in database.
$result = $this->query->where($this->localKey . '.' . $model->getKeyName(), $id)
->update(array($this->localKey . '.$' => $model->getAttributes()));
if ($result)
{
$this->fireModelEvent($model, 'updated', false);
return $model;
}
return false;
}
/**
* Attach a new model without persistence
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function associateNew($model)
{
// Create a new key.
if ( ! $model->getAttribute('_id'))
{
$model->setAttribute('_id', new MongoId);
}
$documents = $this->getEmbeddedRecords();
// Add the document to the parent model.
$documents[] = $model->getAttributes();
$this->setEmbeddedRecords($documents);
// Mark the model as existing.
$model->exists = true;
return $model;
}
/**
* Update an existing model without persistence
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function associateExisting($model)
{
// Get existing embedded documents.
$documents = $this->getEmbeddedRecords();
$primaryKey = $this->related->getKeyName();
$key = $model->getKey();
// Replace the document in the parent model.
foreach ($documents as &$document)
{
if ($document[$primaryKey] == $key)
{
$document = $model->getAttributes();
break;
}
}
$this->setEmbeddedRecords($documents);
return $model;
}
/**
......@@ -406,30 +283,17 @@ class EmbedsMany extends Relation {
{
$ids = $this->getIdsArrayFrom($ids);
$models = $this->find($ids);
$ids = array();
$primaryKey = $this->related->getKeyName();
$models = $this->get()->only($ids);
// Pull the documents from the database.
foreach ($models as $model)
{
if ($this->fireModelEvent($model, 'deleting') === false) continue;
$id = $model->getKey();
$this->query->pull($this->localKey, array($primaryKey => $this->getForeignKeyValue($id)));
$ids[] = $id;
$this->fireModelEvent($model, 'deleted', false);
$this->performDelete($model);
}
return $this->dissociate($ids);
}
/**
* Dissociate the embedded models for the given IDs without persistence.
* Dissociate the model instance from the given parent, without saving it to the database.
*
* @param mixed $ids
* @return int
......@@ -482,6 +346,147 @@ class EmbedsMany extends Relation {
return $this->save($model);
}
/**
* Save a new model and attach it to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function performInsert(Model $model)
{
if ($this->fireModelEvent($model, 'creating') === false) return false;
// Associate the new model to the parent.
$this->associateNew($model);
// Push the new model to the database.
$result = $this->query->push($this->localKey, $model->getAttributes(), true);
if ($result)
{
$this->fireModelEvent($model, 'created', false);
return $model;
}
return false;
}
/**
* Save an existing model and attach it to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return Model|bool
*/
protected function performUpdate(Model $model)
{
if ($this->fireModelEvent($model, 'updating') === false) return false;
// Update the related model in the parent instance
$this->associateExisting($model);
// Get the correct foreign key value.
$id = $this->getForeignKeyValue($model);
// Update document in database.
$result = $this->query->where($this->localKey . '.' . $model->getKeyName(), $id)
->update(array($this->localKey . '.$' => $model->getAttributes()));
if ($result)
{
$this->fireModelEvent($model, 'updated', false);
return $model;
}
return false;
}
/**
* Remove an existing model and detach it from the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return bool
*/
protected function performDelete(Model $model)
{
if ($this->fireModelEvent($model, 'deleting') === false) return false;
// Get the correct foreign key value.
$id = $this->getForeignKeyValue($model);
$result = $this->query->pull($this->localKey, array($model->getKeyName() => $id));
if ($result)
{
$this->fireModelEvent($model, 'deleted', false);
// Update the related model in the parent instance
$this->dissociate($model);
return true;
}
return false;
}
/**
* Associate a new model instance to the given parent, without saving it to the database.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function associateNew($model)
{
// Create a new key.
if ( ! $model->getAttribute('_id'))
{
$model->setAttribute('_id', new MongoId);
}
$documents = $this->getEmbeddedRecords();
// Add the document to the parent model.
$documents[] = $model->getAttributes();
$this->setEmbeddedRecords($documents);
// Mark the model as existing.
$model->exists = true;
return $model;
}
/**
* Associate an existing model instance to the given parent, without saving it to the database.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
protected function associateExisting($model)
{
// Get existing embedded documents.
$documents = $this->getEmbeddedRecords();
$primaryKey = $this->related->getKeyName();
$key = $model->getKey();
// Replace the document in the parent model.
foreach ($documents as &$document)
{
if ($document[$primaryKey] == $key)
{
$document = $model->getAttributes();
break;
}
}
$this->setEmbeddedRecords($documents);
return $model;
}
/**
* Transform single ID, single Model or array of Models into an array of IDs
*
......@@ -501,28 +506,36 @@ class EmbedsMany extends Relation {
}
/**
* Convert an array of embedded documents to a Collection.
* Create a related model instanced.
*
* @param array $results
* @return Illuminate\Database\Eloquent\Collection
* @param array $attributes [description]
* @return [type] [description]
*/
protected function toCollection(array $results = array())
protected function toModel(array $attributes)
{
$models = array();
// Wrap documents in model objects.
foreach ($results as $model)
{
if ( ! $model instanceof Model)
{
$model = $this->related->newFromBuilder($model);
}
$model = $this->related->newFromBuilder($attributes);
// Attatch the parent relation to the embedded model.
$model->setRelation($this->foreignKey, $this->parent);
$model->setHidden(array_merge($model->getHidden(), array($this->foreignKey)));
$models[] = $model;
return $model;
}
/**
* Convert an array of embedded documents to a Collection.
*
* @param array $records
* @return Illuminate\Database\Eloquent\Collection
*/
protected function toCollection(array $records = array())
{
$models = array();
// Wrap records in model objects.
foreach ($records as $attributes)
{
$models[] = $this->toModel($attributes);
}
if (count($models) > 0)
......@@ -534,7 +547,7 @@ class EmbedsMany extends Relation {
}
/**
* Get the embedded documents array.
* Get the embedded records array.
*
* @return array
*/
......@@ -552,7 +565,7 @@ class EmbedsMany extends Relation {
}
/**
* Set the embedded documents array.
* Set the embedded records array.
*
* @param array $models
* @return void
......@@ -598,6 +611,11 @@ class EmbedsMany extends Relation {
*/
protected function getForeignKeyValue($id)
{
if ($id instanceof Model)
{
$id = $id->getKey();
}
// Convert the id to MongoId if necessary.
return $this->getBaseQuery()->convertKey($id);
}
......
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Database\Eloquent\Relations\MorphTo as EloquentMorphTo;
class MorphTo extends BelongsTo {
class MorphTo extends EloquentMorphTo {
/**
* The type of the polymorphic relation.
* Set the base constraints on the relation query.
*
* @var string
*/
protected $morphType;
/**
* The models whose relations are being eager loaded.
*
* @var \Illuminate\Database\Eloquent\Collection
*/
protected $models;
/**
* All of the models keyed by ID.
*
* @var array
*/
protected $dictionary = array();
/**
* Create a new belongs to relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $otherKey
* @param string $type
* @param string $relation
* @return void
*/
public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relation)
public function addConstraints()
{
$this->morphType = $type;
parent::__construct($query, $parent, $foreignKey, $otherKey, $relation);
if (static::$constraints)
{
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$this->query->where($this->otherKey, '=', $this->parent->{$this->foreignKey});
}
}
/**
......@@ -58,125 +32,4 @@ class MorphTo extends BelongsTo {
$this->buildDictionary($this->models = Collection::make($models));
}
/**
* Buiild a dictionary with the models.
*
* @param \Illuminate\Database\Eloquent\Models $models
* @return void
*/
protected function buildDictionary(Collection $models)
{
foreach ($models as $model)
{
if ($model->{$this->morphType})
{
$this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model;
}
}
}
/**
* 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;
}
/**
* Get the results of the relationship.
*
* Called via eager load method of Eloquent query builder.
*
* @return mixed
*/
public function getEager()
{
foreach (array_keys($this->dictionary) as $type)
{
$this->matchToMorphParents($type, $this->getResultsByType($type));
}
return $this->models;
}
/**
* Match the results for a given type to their parents.
*
* @param string $type
* @param \Illuminate\Database\Eloquent\Collection $results
* @return void
*/
protected function matchToMorphParents($type, Collection $results)
{
foreach ($results as $result)
{
if (isset($this->dictionary[$type][$result->getKey()]))
{
foreach ($this->dictionary[$type][$result->getKey()] as $model)
{
$model->setRelation($this->relation, $result);
}
}
}
}
/**
* Get all of the relation results for a type.
*
* @param string $type
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getResultsByType($type)
{
$instance = $this->createModelByType($type);
$key = $instance->getKeyName();
return $instance->whereIn($key, $this->gatherKeysByType($type)->all())->get();
}
/**
* Gather all of the foreign keys for a given type.
*
* @param string $type
* @return array
*/
protected function gatherKeysByType($type)
{
$foreign = $this->foreignKey;
return BaseCollection::make($this->dictionary[$type])->map(function($models) use ($foreign)
{
return head($models)->{$foreign};
})->unique();
}
/**
* Create a new model instance by type.
*
* @param string $type
* @return \Illuminate\Database\Eloquent\Model
*/
public function createModelByType($type)
{
return new $type;
}
/**
* Get the dictionary used by the relationship.
*
* @return array
*/
public function getDictionary()
{
return $this->dictionary;
}
}
......@@ -34,10 +34,14 @@ class ModelTest extends TestCase {
$this->assertEquals(1, User::count());
$this->assertTrue(isset($user->_id));
$this->assertTrue(is_string($user->_id));
$this->assertNotEquals('', (string) $user->_id);
$this->assertNotEquals(0, strlen((string) $user->_id));
$this->assertInstanceOf('Carbon\Carbon', $user->created_at);
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals(35, $user->age);
}
......@@ -50,6 +54,9 @@ class ModelTest extends TestCase {
$user->age = 35;
$user->save();
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$check = User::find($user->_id);
$check->age = 36;
......@@ -65,6 +72,9 @@ class ModelTest extends TestCase {
$user->update(array('age' => 20));
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$check = User::find($user->_id);
$this->assertEquals(20, $check->age);
}
......
......@@ -54,9 +54,7 @@ class QueryBuilderTest extends TestCase {
public function testInsertGetId()
{
$id = DB::collection('users')->insertGetId(array('name' => 'John Doe'));
$this->assertTrue(is_string($id));
$this->assertEquals(24, strlen($id));
$this->assertInstanceOf('MongoId', $id);
}
public function testBatchInsert()
......
......@@ -308,6 +308,10 @@ class RelationsTest extends TestCase {
$this->assertInstanceOf('DateTime', $address->created_at);
$this->assertInstanceOf('DateTime', $address->updated_at);
$this->assertNotNull($address->_id);
$this->assertTrue(is_string($address->_id));
$raw = $address->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$address = $user->addresses()->save(new Address(array('city' => 'Paris')));
......@@ -410,15 +414,21 @@ class RelationsTest extends TestCase {
$user = User::create(array());
$address = $user->addresses()->create(array('city' => 'Bruxelles'));
$this->assertInstanceOf('Address', $address);
$this->assertInstanceOf('MongoID', $address->_id);
$this->assertTrue(is_string($address->_id));
$this->assertEquals(array('Bruxelles'), $user->addresses->lists('city'));
$raw = $address->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$freshUser = User::find($user->id);
$this->assertEquals(array('Bruxelles'), $freshUser->addresses->lists('city'));
$user = User::create(array());
$address = $user->addresses()->create(array('_id' => '', 'city' => 'Bruxelles'));
$this->assertInstanceOf('MongoID', $address->_id);
$this->assertTrue(is_string($address->_id));
$raw = $address->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
}
public function testEmbedsManyCreateMany()
......@@ -553,4 +563,20 @@ class RelationsTest extends TestCase {
$address->unsetEventDispatcher();
}
public function testEmbedsManyFindOrContains()
{
$user = User::create(array('name' => 'John Doe'));
$address1 = $user->addresses()->save(new Address(array('city' => 'New York')));
$address2 = $user->addresses()->save(new Address(array('city' => 'Paris')));
$address = $user->addresses()->find($address1->_id);
$this->assertEquals($address->city, $address1->city);
$address = $user->addresses()->find($address2->_id);
$this->assertEquals($address->city, $address2->city);
$this->assertTrue($user->addresses()->contains($address2->_id));
$this->assertFalse($user->addresses()->contains('123'));
}
}
......@@ -32,6 +32,9 @@ class TestCase extends Orchestra\Testbench\TestCase {
// overwrite database configuration
$app['config']->set('database.connections.mysql', $config['connections']['mysql']);
$app['config']->set('database.connections.mongodb', $config['connections']['mongodb']);
// overwrite cache configuration
$app['config']->set('cache.driver', 'array');
}
}
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