Commit 0a256970 authored by Jens Segers's avatar Jens Segers

Merge pull request #279 from jenssegers/embedded-models

New v2 release
parents 7534e6f9 94314570
......@@ -3,7 +3,17 @@ Laravel MongoDB
[![Latest Stable Version](http://img.shields.io/github/release/jenssegers/laravel-mongodb.svg)](https://packagist.org/packages/jenssegers/mongodb) [![Total Downloads](http://img.shields.io/packagist/dm/jenssegers/mongodb.svg)](https://packagist.org/packages/jenssegers/mongodb) [![Build Status](http://img.shields.io/travis/jenssegers/laravel-mongodb.svg)](https://travis-ci.org/jenssegers/laravel-mongodb) [![Coverage Status](http://img.shields.io/coveralls/jenssegers/laravel-mongodb.svg)](https://coveralls.io/r/jenssegers/laravel-mongodb?branch=master)
An Eloquent model and Query builder with support for MongoDB, inspired by LMongo, but using the original Laravel methods. *This library extends the original Laravel classes, so it uses exactly the same methods.*
An Eloquent model and Query builder with support for MongoDB, using the original Laravel API. *This library extends the original Laravel classes, so it uses exactly the same methods.*
### Upgrading from v1 to v2
In this new version, embedded documents are no longer saved to the parent model using an attribute with a leading underscore. If you have a relation like `embedsMany('Book')`, these books are now stored under `$model['books']` instead of `$model['_books']`. This was changed to make embedded relations less confusing for new developers.
If you want to upgrade to this new version without having to change all your existing database objects, you can modify your embedded relations to use a non-default local key including the underscore:
$this->embedsMany('Book', '_books');
Read the full changelog at https://github.com/jenssegers/laravel-mongodb/releases/tag/v2.0.0
Installation
------------
......@@ -65,7 +75,7 @@ Tell your model to use the MongoDB model and set the collection (alias for table
}
If you are using a different database driver as the default one, you will need to specify the mongodb connection within your model by changing the `connection` property:
If you are using a different database driver as the default one, you will need to specify the mongodb connection name within your model by changing the `connection` property:
use Jenssegers\Mongodb\Model as Eloquent;
......@@ -94,15 +104,15 @@ This will allow you to use your registered alias like:
Query Builder
-------------
The database driver plugs right into the original query builder. When using mongodb connections you will be able to build fluent queries to perform database operations. For your convenience, there is a `collection` alias for `table` as well as some additional mongodb specific operators/operations.
// With custom connection
$user = DB::connection('mongodb')->collection('users')->get();
The database driver plugs right into the original query builder. When using mongodb connections, you will be able to build fluent queries to perform database operations. For your convenience, there is a `collection` alias for `table` as well as some additional mongodb specific operators/operations.
// Using default connection
$users = DB::collection('users')->get();
$user = DB::collection('users')->where('name', 'John')->first();
If you did not change your default database connection, you will need to specify it when querying.
$user = DB::connection('mongodb')->collection('users')->get();
Read more about the query builder on http://laravel.com/docs/queries
Schema
......@@ -258,15 +268,15 @@ You may also specify additional columns to update:
When soft deleting a model, it is not actually removed from your database. Instead, a deleted_at timestamp is set on the record. To enable soft deletes for a model, apply the SoftDeletingTrait to the model:
use Jenssegers\Mongodb\Eloquent\SoftDeletingTrait;
use Jenssegers\Mongodb\Eloquent\SoftDeletingTrait;
class User extends Eloquent {
class User extends Eloquent {
use SoftDeletingTrait;
protected $dates = ['deleted_at'];
}
}
For more information check http://laravel.com/docs/eloquent#soft-deleting
......@@ -314,7 +324,40 @@ Matches documents that satisfy a JavaScript expression. For more information che
### Inserts, updates and deletes
All basic insert, update, delete and select methods should be implemented.
Inserting, updating and deleting records works just like the original Eloquent.
**Saving a new model**
$user = new User;
$user->name = 'John';
$user->save();
You may also use the create method to save a new model in a single line:
User::create(array('name' => 'John'));
**Updating a model**
o update a model, you may retrieve it, change an attribute, and use the save method.
$user = User::first();
$user->email = 'john@foo.com';
$user->save();
*There is also support for upsert operations, check https://github.com/jenssegers/laravel-mongodb#mongodb-specific-operations*
**Deleting a model**
To delete a model, simply call the delete method on the instance:
$user = User::first();
$user->delete();
Or deleting a model by its key:
User::destroy('517c43667db388101e00000f');
For more information about model manipulation, check http://laravel.com/docs/eloquent#insert-update-delete
### Dates
......@@ -386,7 +429,7 @@ Other relations are not yet supported, but may be added in the future. Read more
### EmbedsMany Relations
If you want to embed documents, rather than referencing them, you can use the `embedsMany` relation:
If you want to embed models, rather than referencing them, you can use the `embedsMany` relation. This relation is similar to the `hasMany` relation, but embeds the models inside the parent object.
use Jenssegers\Mongodb\Model as Eloquent;
......@@ -399,52 +442,71 @@ If you want to embed documents, rather than referencing them, you can use the `e
}
Now we can access the user's books through the dynamic property:
You access the embedded models through the dynamic property:
$books = User::first()->books;
When using embedded documents, there will also be an inverse relation available:
The inverse relation is auto*magically* available, you don't need to define this reverse relation.
$user = $book->user;
Inserting and updating embedded documents works just like the `belongsTo` relation:
Inserting and updating embedded models works similar to the `hasMany` relation:
$book = new Book(array('title' => 'A Game of Thrones'));
$user = User::first();
$book = $user->books()->save($book);
// or
$book = $user->books()->create(array('title' => 'A Game of Thrones'))
You can remove an embedded document by using the `destroy()` method:
You can update embedded models using their `save` method (available since release 2.0.0):
$book = $user->books()->first();
$user->books()->destroy($book->_id);
$book->title = 'A Game of Thrones';
$book->save();
You can remove an embedded model by using the `destroy` method on the relation, or the `delete` method on the model (available since release 2.0.0):
$book = $user->books()->first();
$book->delete();
// or
$user->books()->destroy($book);
If you want to add or remove embedded documents, without persistence, you can use the `associate` and `dissociate` methods. To write the changes to the database, save the parent object:
If you want to add or remove an embedded model, without touching the database, you can use the `associate` and `dissociate` methods. To eventually write the changes to the database, save the parent object:
$user->books()->associate($book);
$user->save();
Again, you may override the conventional local key by passing a second argument to the embedsMany method:
Like other relations, embedsMany assumes the local key of the relationship based on the model name. You can override the default local key by passing a second argument to the embedsMany method:
return $this->embedsMany('Book', 'local_key');
When using embedded documents, they will be stored in a _relation attribute of the parent document. This attribute is hidden by default when using `toArray` or `toJson`. If you want the attribute to be exposed, add it to `$exposed` property definition to your model:
Embedded relations will return a Collection of embedded items instead of a query builder. To allow a more query-like behavior, embedded relations will return a modified version of the Collection class with support for the following **additional** operations:
use Jenssegers\Mongodb\Model as Eloquent;
- where($key, $operator, $value)
- whereIn($key, $values) and whereNotIn($key, $values)
- whereBetween($key, $values) and whereNotBetween($key, $values)
- whereNull($key) and whereNotNull($key)
- orderBy($key, $direction)
- oldest() and latest()
- limit($value)
- offset($value)
- skip($value)
class User extends Eloquent {
This allows you to execute simple queries on the collection results:
protected $exposed = array('_books');
$books = $user->books()->where('rating', '>', 5)->orderBy('title')->get();
}
**Note:** Because embedded models are not stored in a separate collection, you can not query all of embedded models. You will always have to access them through the parent model.
### EmbedsOne Relations
There is also an EmbedsOne relation, which works similar to the EmbedsMany relation, but only stores one embedded model.
The embedsOne relation is similar to the EmbedsMany relation, but only embeds a single model.
use Jenssegers\Mongodb\Model as Eloquent;
......@@ -457,17 +519,31 @@ There is also an EmbedsOne relation, which works similar to the EmbedsMany relat
}
Now we can access the book's author through the dynamic property:
You access the embedded models through the dynamic property:
$author = Book::first()->author;
Inserting and updating embedded documents works just like the `embedsMany` relation:
Inserting and updating embedded models works similar to the `hasOne` relation:
$author = new Author(array('name' => 'John Doe'));
$book = Books::first();
$author = $user->author()->save($author);
$author = $book->author()->save($author);
// or
$author = $book->author()->create(array('name' => 'John Doe'));
You can update the embedded model using the `save` method (available since release 2.0.0):
$author = $book->author;
$author->name = 'Jane Doe';
$author->save();
You can replace the embedded model with a new model like this:
$newAuthor = new Author(array('name' => 'Jane Doe'));
$book->author()->save($newAuthor);
### MySQL Relations
......@@ -527,13 +603,19 @@ Optional: if you don't pass a closure to the raw method, the internal MongoColle
$model = User::raw()->findOne(array('age' => array('$lt' => 18)));
The MongoClient and MongoDB objects can be accessed like this:
The internal MongoClient and MongoDB objects can be accessed like this:
$client = DB::getMongoClient();
$db = DB::getMongoDB();
### MongoDB specific operations
**Cursor timeout**
To prevent MongoCursorTimeout exceptions, you can manually set a timeout value that will be applied to the cursor:
DB::collection('users')->timeout(-1)->get();
**Upsert**
Update or insert a document. Additional options for the update method are passed directly to the native update method.
......@@ -541,6 +623,12 @@ Update or insert a document. Additional options for the update method are passed
DB::collection('users')->where('name', 'John')
->update($data, array('upsert' => true));
**Projections**
You can apply projections to your queries using the `project` method.
DB::collection('items')->project(array('tags' => array('$slice' => 1)))->get();
**Push**
Add an items to an array.
......
<?php namespace Jenssegers\Mongodb\Auth;
use DateTime;
use MongoDate;
class DatabaseReminderRepository extends \Illuminate\Auth\Reminders\DatabaseReminderRepository {
/**
* Build the record payload for the table.
*
* @param string $email
* @param string $token
* @return array
*/
protected function getPayload($email, $token)
{
return array('email' => $email, 'token' => $token, 'created_at' => new MongoDate);
}
/**
* Determine if the reminder has expired.
*
......@@ -10,16 +25,22 @@ class DatabaseReminderRepository extends \Illuminate\Auth\Reminders\DatabaseRemi
*/
protected function reminderExpired($reminder)
{
// Convert to array so that we can pass it to the parent method
if (is_object($reminder))
// Convert MongoDate to a date string.
if ($reminder['created_at'] instanceof MongoDate)
{
$reminder = (array) $reminder;
$date = new DateTime;
$date->setTimestamp($reminder['created_at']->sec);
$reminder['created_at'] = $date->format('Y-m-d H:i:s');
}
// Convert the DateTime object that got saved to MongoDB
if (is_array($reminder['created_at']))
// Convert DateTime to a date string (backwards compatibility).
elseif (is_array($reminder['created_at']))
{
$reminder['created_at'] = $reminder['created_at']['date'] + $reminder['created_at']['timezone'];
$date = DateTime::__set_state($reminder['created_at']);
$reminder['created_at'] = $date->format('Y-m-d H:i:s');
}
return parent::reminderExpired($reminder);
......
......@@ -26,6 +26,7 @@ class Collection {
public function __construct(Connection $connection, MongoCollection $collection)
{
$this->connection = $connection;
$this->collection = $collection;
}
......@@ -38,29 +39,34 @@ class Collection {
*/
public function __call($method, $parameters)
{
$query = array();
// Build the query string.
$query = $parameters;
foreach ($query as &$param)
foreach ($parameters as $parameter)
{
try
{
$param = json_encode($param);
$query[] = json_encode($parameter);
}
catch (Exception $e)
{
$param = '{...}';
$query[] = '{...}';
}
}
$start = microtime(true);
// Execute the query.
$result = call_user_func_array(array($this->collection, $method), $parameters);
// Log the query.
$this->connection->logQuery(
$this->collection->getName() . '.' . $method . '(' . join(',', $query) . ')',
array(), $this->connection->getElapsedTime($start));
// Once we have run the query we will calculate the time that it took to run and
// then log the query, bindings, and execution time so we will report them on
// the event that the developer needs them. We'll log time in milliseconds.
$time = $this->connection->getElapsedTime($start);
// Convert the query to a readable string.
$queryString = $this->collection->getName() . '.' . $method . '(' . join(',', $query) . ')';
$this->connection->logQuery($queryString, array(), $time);
return $result;
}
......
......@@ -16,6 +16,146 @@ class Builder extends EloquentBuilder {
'count', 'min', 'max', 'avg', 'sum', 'exists', 'push', 'pull'
);
/**
* Update a record in the database.
*
* @param array $values
* @return int
*/
public function update(array $values)
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$relation->performUpdate($this->model, $values);
return 1;
}
return parent::update($values);
}
/**
* Insert a new record into the database.
*
* @param array $values
* @return bool
*/
public function insert(array $values)
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$relation->performInsert($this->model, $values);
return true;
}
return parent::insert($values);
}
/**
* Insert a new record and get the value of the primary key.
*
* @param array $values
* @param string $sequence
* @return int
*/
public function insertGetId(array $values, $sequence = null)
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$relation->performInsert($this->model, $values);
return $this->model->getKey();
}
return parent::insertGetId($values, $sequence);
}
/**
* Delete a record from the database.
*
* @return mixed
*/
public function delete()
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$relation->performDelete($this->model);
return $this->model->getKey();
}
return parent::delete();
}
/**
* Increment a column's value by a given amount.
*
* @param string $column
* @param int $amount
* @param array $extra
* @return int
*/
public function increment($column, $amount = 1, array $extra = array())
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$value = $this->model->{$column};
// When doing increment and decrements, Eloquent will automatically
// sync the original attributes. We need to change the attribute
// temporary in order to trigger an update query.
$this->model->{$column} = null;
$this->model->syncOriginalAttribute($column);
$result = $this->model->update(array($column => $value));
return $result;
}
return parent::increment($column, $amount, $extra);
}
/**
* Decrement a column's value by a given amount.
*
* @param string $column
* @param int $amount
* @param array $extra
* @return int
*/
public function decrement($column, $amount = 1, array $extra = array())
{
// Intercept operations on embedded models and delegate logic
// to the parent relation instance.
if ($relation = $this->model->getParentRelation())
{
$value = $this->model->{$column};
// When doing increment and decrements, Eloquent will automatically
// sync the original attributes. We need to change the attribute
// temporary in order to trigger an update query.
$this->model->{$column} = null;
$this->model->syncOriginalAttribute($column);
return $this->model->update(array($column => $value));
}
return parent::decrement($column, $amount, $extra);
}
/**
* Add the "has" condition where clause to the query.
*
......@@ -77,26 +217,12 @@ class Builder extends EloquentBuilder {
// Get raw results from the query builder.
$results = $this->query->raw($expression);
$connection = $this->model->getConnectionName();
// Convert MongoCursor results to a collection of models.
if ($results instanceof MongoCursor)
{
$results = iterator_to_array($results, false);
$models = array();
// Once we have the results, we can spin through them and instantiate a fresh
// model instance for each records we retrieved from the database. We will
// also set the proper connection name for the model after we create it.
foreach ($results as $result)
{
$models[] = $model = $this->model->newFromBuilder($result);
$model->setConnection($connection);
}
return $this->model->newCollection($models);
return $this->model->hydrate($results);
}
// The result is a single object.
......@@ -104,7 +230,7 @@ class Builder extends EloquentBuilder {
{
$model = $this->model->newFromBuilder($results);
$model->setConnection($connection);
$model->setConnection($this->model->getConnection());
return $model;
}
......
<?php namespace Jenssegers\Mongodb\Eloquent;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
class Collection extends EloquentCollection {
/**
* Simulate a get clause on the collection.
*
* @param mixed $key
* @param mixed $default
* @return mixed
*/
public function get($key = null, $default = null)
{
if (is_null($key) and is_null($default))
{
return $this;
}
return parent::get($key, $default);
}
/**
* Simulate a basic where clause on the collection.
*
* @param string $key
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function where($key, $operator = null, $value = null)
{
// Here we will make some assumptions about the operator. If only 2 values are
// passed to the method, we will assume that the operator is an equals sign
// and keep going.
if (func_num_args() == 2)
{
list($value, $operator) = array($operator, '=');
}
return $this->filter(function($item) use ($key, $operator, $value)
{
$actual = $item->{$key};
switch ($operator)
{
case '<>':
case '!=':
return $actual != $value;
break;
case '>':
return $actual > $value;
break;
case '<':
return $actual < $value;
break;
case '>=':
return $actual >= $value;
break;
case '<=':
return $actual <= $value;
break;
case 'between':
return $actual >= $value[0] and $actual <= $value[1];
break;
case 'not between':
return $actual < $value[0] or $actual > $value[1];
break;
case 'in':
return in_array($actual, $value);
break;
case 'not in':
return ! in_array($actual, $value);
break;
case '=':
default:
return $actual == $value;
break;
}
});
}
/**
* Add a where between statement to the query.
*
* @param string $column
* @param array $values
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereBetween($column, array $values, $boolean = 'and', $not = false)
{
$type = $not ? 'not between' : 'between';
return $this->where($column, $type, $values);
}
/**
* Add a where not between statement to the query.
*
* @param string $column
* @param array $values
* @param string $boolean
* @return $this
*/
public function whereNotBetween($column, array $values, $boolean = 'and')
{
return $this->whereBetween($column, $values, $boolean, true);
}
/**
* Add a "where in" clause to the query.
*
* @param string $column
* @param mixed $values
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereIn($column, $values, $boolean = 'and', $not = false)
{
$type = $not ? 'not in' : 'in';
return $this->where($column, $type, $values);
}
/**
* Add a "where not in" clause to the query.
*
* @param string $column
* @param mixed $values
* @param string $boolean
* @return $this
*/
public function whereNotIn($column, $values, $boolean = 'and')
{
return $this->whereIn($column, $values, $boolean, true);
}
/**
* Add a "where null" clause to the query.
*
* @param string $column
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereNull($column, $boolean = 'and', $not = false)
{
return $this->where($column, '=', null);
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
* @param string $boolean
* @return $this
*/
public function whereNotNull($column, $boolean = 'and')
{
return $this->where($column, '!=', null);
}
/**
* Simulate order by clause on the collection.
*
* @param string $key
* @param string $direction
* @return $this
*/
public function orderBy($key, $direction = 'asc')
{
$descending = strtolower($direction) == 'desc';
return $this->sortBy($key, SORT_REGULAR, $descending);
}
/**
* Add an "order by" clause for a timestamp to the query.
*
* @param string $column
* @return $this
*/
public function latest($column = 'created_at')
{
return $this->orderBy($column, 'desc');
}
/**
* Add an "order by" clause for a timestamp to the query.
*
* @param string $column
* @return $this
*/
public function oldest($column = 'created_at')
{
return $this->orderBy($column, 'asc');
}
/**
* Set the "offset" value of the query.
*
* @param int $value
* @return $this
*/
public function offset($value)
{
$offset = max(0, $value);
return $this->slice($offset);
}
/**
* Alias to set the "offset" value of the query.
*
* @param int $value
* @return $this
*/
public function skip($value)
{
return $this->offset($value);
}
/**
* Set the "limit" value of the query.
*
* @param int $value
* @return $this
*/
public function limit($value)
{
return $this->take($value);
}
}
......@@ -4,6 +4,8 @@ use Illuminate\Database\Eloquent\Collection;
use Jenssegers\Mongodb\DatabaseManager as Resolver;
use Jenssegers\Mongodb\Eloquent\Builder;
use Jenssegers\Mongodb\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Jenssegers\Mongodb\Relations\EmbedsOneOrMany;
use Jenssegers\Mongodb\Relations\EmbedsMany;
use Jenssegers\Mongodb\Relations\EmbedsOne;
......@@ -29,11 +31,11 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
protected $primaryKey = '_id';
/**
* The attributes that should be exposed for toArray and toJson.
* The parent relation instance.
*
* @var array
* @var Relation
*/
protected $exposed = array();
protected $parentRelation;
/**
* The connection resolver instance.
......@@ -88,7 +90,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
if (is_null($localKey))
{
$localKey = '_' . $relation;
$localKey = $relation;
}
if (is_null($foreignKey))
......@@ -124,7 +126,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
if (is_null($localKey))
{
$localKey = '_' . $relation;
$localKey = $relation;
}
if (is_null($foreignKey))
......@@ -220,7 +222,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
public function getAttribute($key)
{
// Check if the key is an array dot notation.
if (strpos($key, '.') !== false)
if (str_contains($key, '.'))
{
$attributes = array_dot($this->attributes);
......@@ -230,6 +232,32 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
}
}
$camelKey = camel_case($key);
// If the "attribute" exists as a method on the model, it may be an
// embedded model. If so, we need to return the result before it
// is handled by the parent method.
if (method_exists($this, $camelKey))
{
$relations = $this->$camelKey();
// This attribute matches an embedsOne or embedsMany relation so we need
// to return the relation results instead of the interal attributes.
if ($relations instanceof EmbedsOneOrMany)
{
// 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.
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
// Get the relation results.
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
return parent::getAttribute($key);
}
......@@ -241,12 +269,8 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
*/
protected function getAttributeFromArray($key)
{
if (array_key_exists($key, $this->attributes))
{
return $this->attributes[$key];
}
else if (strpos($key, '.') !== false)
// Support keys in dot notation.
if (str_contains($key, '.'))
{
$attributes = array_dot($this->attributes);
......@@ -255,6 +279,8 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
return $attributes[$key];
}
}
return parent::getAttributeFromArray($key);
}
/**
......@@ -270,9 +296,21 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
if ($key == '_id' and is_string($value))
{
$builder = $this->newBaseQueryBuilder();
$value = $builder->convertKey($value);
}
// Support keys in dot notation.
elseif (str_contains($key, '.'))
{
if (in_array($key, $this->getDates()) && $value)
{
$value = $this->fromDateTime($value);
}
array_set($this->attributes, $key, $value); return;
}
parent::setAttribute($key, $value);
}
......@@ -295,22 +333,6 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
{
$value = (string) $value;
}
// If the attribute starts with an underscore, it might be the
// internal array of embedded documents. In that case, we need
// to hide these from the output so that the relation-based
// attribute can take over.
else if (starts_with($key, '_') and ! in_array($key, $this->exposed))
{
$camelKey = camel_case($key);
// If we can find a method that responds to this relation we
// will remove it from the output.
if (method_exists($this, $camelKey))
{
unset($attributes[$key]);
}
}
}
return $attributes;
......@@ -345,9 +367,25 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
{
if ($parameters = func_get_args())
{
$unique = false;
if (count($parameters) == 3)
{
list($column, $values, $unique) = $parameters;
}
else
{
list($column, $values) = $parameters;
}
// Do batch push by default.
if ( ! is_array($values)) $values = array($values);
$query = $this->setKeysForSaveQuery($this->newQuery());
return call_user_func_array(array($query, 'push'), $parameters);
$this->pushAttributeValues($column, $values, $unique);
return $query->push($column, $values, $unique);
}
return parent::push();
......@@ -358,22 +396,87 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
*
* @return mixed
*/
public function pull()
public function pull($column, $values)
{
// Do batch pull by default.
if ( ! is_array($values)) $values = array($values);
$query = $this->setKeysForSaveQuery($this->newQuery());
return call_user_func_array(array($query, 'pull'), func_get_args());
$this->pullAttributeValues($column, $values);
return $query->pull($column, $values);
}
/**
* Set the exposed attributes for the model.
* Append one or more values to the underlying attribute value and sync with original.
*
* @param array $exposed
* @param string $column
* @param array $values
* @param bool $unique
* @return void
*/
public function setExposed(array $exposed)
protected function pushAttributeValues($column, array $values, $unique = false)
{
$current = $this->getAttributeFromArray($column) ?: array();
foreach ($values as $value)
{
// Don't add duplicate values when we only want unique values.
if ($unique and in_array($value, $current)) continue;
array_push($current, $value);
}
$this->attributes[$column] = $current;
$this->syncOriginalAttribute($column);
}
/**
* Rempove one or more values to the underlying attribute value and sync with original.
*
* @param string $column
* @param array $values
* @return void
*/
protected function pullAttributeValues($column, array $values)
{
$current = $this->getAttributeFromArray($column) ?: array();
foreach ($values as $value)
{
$keys = array_keys($current, $value);
foreach ($keys as $key)
{
unset($current[$key]);
}
}
$this->attributes[$column] = array_values($current);
$this->syncOriginalAttribute($column);
}
/**
* Set the parent relation.
*
* @param Relation $relation
*/
public function setParentRelation(Relation $relation)
{
$this->parentRelation = $relation;
}
/**
* Get the parent relation.
*
* @return Relation
*/
public function getParentRelation()
{
$this->exposed = $exposed;
return $this->parentRelation;
}
/**
......
......@@ -6,10 +6,11 @@ use MongoDate;
use DateTime;
use Closure;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression;
use Jenssegers\Mongodb\Connection;
class Builder extends \Illuminate\Database\Query\Builder {
class Builder extends QueryBuilder {
/**
* The database collection
......@@ -18,6 +19,20 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
protected $collection;
/**
* The column projections.
*
* @var array
*/
public $projections;
/**
* The cursor timeout value.
*
* @var int
*/
public $timeout;
/**
* All of the available clause operators.
*
......@@ -59,6 +74,32 @@ class Builder extends \Illuminate\Database\Query\Builder {
$this->connection = $connection;
}
/**
* Set the projections.
*
* @param array $columns
* @return $this
*/
public function project($columns)
{
$this->projections = is_array($columns) ? $columns : func_get_args();
return $this;
}
/**
* Set the cursor timeout in seconds.
*
* @param int $seconds
* @return $this
*/
public function timeout($seconds)
{
$this->timeout = $seconds;
return $this;
}
/**
* Execute a query for a single record by ID.
*
......@@ -71,6 +112,17 @@ class Builder extends \Illuminate\Database\Query\Builder {
return $this->where('_id', '=', $this->convertKey($id))->first($columns);
}
/**
* Execute the query as a "select" statement.
*
* @param array $columns
* @return array|static[]
*/
public function get($columns = array())
{
return parent::get($columns);
}
/**
* Execute the query as a fresh "select" statement.
*
......@@ -142,6 +194,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
foreach ($this->columns as $column)
{
$key = str_replace('.', '_', $column);
$group[$key] = array('$last' => '$' . $column);
}
}
......@@ -155,6 +208,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
if ($this->orders) $pipeline[] = array('$sort' => $this->orders);
if ($this->offset) $pipeline[] = array('$skip' => $this->offset);
if ($this->limit) $pipeline[] = array('$limit' => $this->limit);
if ($this->projections) $pipeline[] = array('$project' => $this->projections);
// Execute aggregation
$results = $this->collection->aggregate($pipeline);
......@@ -179,15 +233,24 @@ class Builder extends \Illuminate\Database\Query\Builder {
else
{
$columns = array();
// Convert select columns to simple projections.
foreach ($this->columns as $column)
{
$columns[$column] = true;
}
// Add custom projections.
if ($this->projections)
{
$columns = array_merge($columns, $this->projections);
}
// Execute query and get MongoCursor
$cursor = $this->collection->find($wheres, $columns);
// Apply order, offset and limit
if ($this->timeout) $cursor->timeout($this->timeout);
if ($this->orders) $cursor->sort($this->orders);
if ($this->offset) $cursor->skip($this->offset);
if ($this->limit) $cursor->limit($this->limit);
......@@ -313,17 +376,18 @@ class Builder extends \Illuminate\Database\Query\Builder {
// Since every insert gets treated like a batch insert, we will have to detect
// if the user is inserting a single document or an array of documents.
$batch = true;
foreach ($values as $value)
{
// As soon as we find a value that is not an array we assume the user is
// inserting a single document.
if (!is_array($value))
if ( ! is_array($value))
{
$batch = false; break;
}
}
if (!$batch) $values = array($values);
if ( ! $batch) $values = array($values);
// Batch insert
$result = $this->collection->batchInsert($values);
......@@ -344,7 +408,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
if (1 == (int) $result['ok'])
{
if (!$sequence)
if (is_null($sequence))
{
$sequence = '_id';
}
......@@ -363,7 +427,13 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
public function update(array $values, array $options = array())
{
return $this->performUpdate(array('$set' => $values), $options);
// Use $set as default operator.
if ( ! starts_with(key($values), '$'))
{
$values = array('$set' => $values);
}
return $this->performUpdate($values, $options);
}
/**
......@@ -376,11 +446,9 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
public function increment($column, $amount = 1, array $extra = array(), array $options = array())
{
$query = array(
'$inc' => array($column => $amount)
);
$query = array('$inc' => array($column => $amount));
if (!empty($extra))
if ( ! empty($extra))
{
$query['$set'] = $extra;
}
......@@ -389,6 +457,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
$this->where(function($query) use ($column)
{
$query->where($column, 'exists', false);
$query->orWhereNotNull($column);
});
......@@ -437,6 +506,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
public function delete($id = null)
{
$wheres = $this->compileWheres();
$result = $this->collection->remove($wheres);
if (1 == (int) $result['ok'])
......@@ -490,7 +560,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
}
// Create an expression for the given value
else if (!is_null($expression))
else if ( ! is_null($expression))
{
return new Expression($expression);
}
......@@ -511,10 +581,17 @@ class Builder extends \Illuminate\Database\Query\Builder {
// Use the addToSet operator in case we only want unique items.
$operator = $unique ? '$addToSet' : '$push';
// Check if we are pushing multiple values.
$batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1));
if (is_array($column))
{
$query = array($operator => $column);
}
else if ($batch)
{
$query = array($operator => array($column => array('$each' => $value)));
}
else
{
$query = array($operator => array($column => $value));
......@@ -532,13 +609,19 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
public function pull($column, $value = null)
{
// Check if we passed an associative array.
$batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1));
// If we are pulling multiple values, we need to use $pullAll.
$operator = $batch ? '$pullAll' : '$pull';
if (is_array($column))
{
$query = array('$pull' => $column);
$query = array($operator => $column);
}
else
{
$query = array('$pull' => array($column => $value));
$query = array($operator => array($column => $value));
}
return $this->performUpdate($query);
......@@ -552,7 +635,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
public function drop($columns)
{
if (!is_array($columns)) $columns = array($columns);
if ( ! is_array($columns)) $columns = array($columns);
$fields = array();
......@@ -585,13 +668,14 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
protected function performUpdate($query, array $options = array())
{
// Default options
$default = array('multiple' => true);
// Merge options and override default options
$options = array_merge($default, $options);
// Update multiple items by default.
if ( ! array_key_exists('multiple', $options))
{
$options['multiple'] = true;
}
$wheres = $this->compileWheres();
$result = $this->collection->update($wheres, $query, $options);
if (1 == (int) $result['ok'])
......@@ -682,7 +766,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
}
// Convert id's.
if (isset($where['column']) and $where['column'] == '_id')
if (isset($where['column']) and ($where['column'] == '_id' or ends_with($where['column'], '._id')))
{
// Multiple values.
if (isset($where['values']))
......
<?php namespace Jenssegers\Mongodb\Relations;
use Jenssegers\Mongodb\Model;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentBelongsToMany;
......@@ -40,15 +40,58 @@ class BelongsToMany extends EloquentBelongsToMany {
}
}
/**
* 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)
{
$model->save(array('touch' => false));
$this->attach($model, $joining, $touch);
return $model;
}
/**
* 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);
// Once we save the related model, we need to attach it to the base model via
// through intermediate table so we'll use the existing "attach" method to
// accomplish this which will insert the record and any more attributes.
$instance->save(array('touch' => false));
$this->attach($instance, $joining, $touch);
return $instance;
}
/**
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param array $ids
* @param mixed $ids
* @param bool $detaching
* @return void
* @return array
*/
public function sync($ids, $detaching = true)
{
$changes = array(
'attached' => array(), 'detached' => array(), 'updated' => array()
);
if ($ids instanceof Collection) $ids = $ids->modelKeys();
// First we need to attach any of the associated models that are not currently
......@@ -56,6 +99,9 @@ class BelongsToMany extends EloquentBelongsToMany {
// if they exist in the array of current ones, and if not we will insert.
$current = $this->parent->{$this->otherKey} ?: array();
// See issue #256.
if ($current instanceof Collection) $current = $ids->modelKeys();
$records = $this->formatSyncList($ids);
$detach = array_diff($current, array_keys($records));
......@@ -66,36 +112,36 @@ class BelongsToMany extends EloquentBelongsToMany {
if ($detaching and count($detach) > 0)
{
$this->detach($detach);
$changes['detached'] = (array) array_map('intval', $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);
$changes = array_merge(
$changes, $this->attachNew($records, $current, false)
);
if (count($changes['attached']) || count($changes['updated']))
{
$this->touchIfTouching();
}
return $changes;
}
/**
* Attach all of the IDs that aren't in the current array.
* Update an existing pivot record on the table.
*
* @param array $records
* @param array $current
* @param mixed $id
* @param array $attributes
* @param bool $touch
* @return void
*/
protected function attachNew(array $records, array $current, $touch = true)
{
foreach ($records as $id => $attributes)
public function updateExistingPivot($id, array $attributes, $touch = true)
{
// 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);
}
}
// Do nothing, we have no pivot table.
}
/**
......@@ -108,25 +154,37 @@ class BelongsToMany extends EloquentBelongsToMany {
*/
public function attach($id, array $attributes = array(), $touch = true)
{
if ($id instanceof Model) $id = $id->getKey();
if ($id instanceof Model)
{
$model = $id; $id = $model->getKey();
}
$records = $this->createAttachRecords((array) $id, $attributes);
// Get the ID's to attach to the two documents
// Get the ids to attach to the parent and related model.
$otherIds = array_pluck($records, $this->otherKey);
$foreignIds = array_pluck($records, $this->foreignKey);
// Attach to the parent model
$this->parent->push($this->otherKey, $otherIds[0]);
// Attach the new ids to the parent model.
$this->parent->push($this->otherKey, $otherIds, true);
// Generate a new related query instance
$query = $this->getNewRelatedQuery();
// If we have a model instance, we can psuh the ids to that model,
// so that the internal attributes are updated as well. Otherwise,
// we will just perform a regular database query.
if (isset($model))
{
// Attach the new ids to the related model.
$model->push($this->foreignKey, $foreignIds, true);
}
else
{
$query = $this->newRelatedQuery();
// Set contraints on the related query
$query->where($this->related->getKeyName(), $id);
// Attach to the related model
$query->push($this->foreignKey, $foreignIds[0]);
// Attach the new ids to the related model.
$query->push($this->foreignKey, $foreignIds, true);
}
if ($touch) $this->touchIfTouching();
}
......@@ -142,18 +200,15 @@ class BelongsToMany extends EloquentBelongsToMany {
{
if ($ids instanceof Model) $ids = (array) $ids->getKey();
$query = $this->getNewRelatedQuery();
$query = $this->newRelatedQuery();
// 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;
// Pull each id from the parent.
foreach ($ids as $id)
{
$this->parent->pull($this->otherKey, $id);
}
// Detach all ids from the parent model.
$this->parent->pull($this->otherKey, $ids);
// Prepare the query to select all related objects.
if (count($ids) > 0)
......@@ -196,11 +251,21 @@ class BelongsToMany extends EloquentBelongsToMany {
}
/**
* Get a new related query.
* Create a new query builder for the related model.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newPivotQuery()
{
return $this->newRelatedQuery();
}
/**
* Create a new query builder for the related model.
*
* @return \Illuminate\Database\Query\Builder
*/
public function getNewRelatedQuery()
public function newRelatedQuery()
{
return $this->related->newQuery();
}
......
<?php namespace Jenssegers\Mongodb\Relations;
use MongoId;
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 EmbedsOneOrMany {
/**
* Get the results of the relationship.
*
* @return Illuminate\Database\Eloquent\Collection
* @return Jenssegers\Mongodb\Eloquent\Collection
*/
public function getResults()
{
......@@ -19,54 +18,109 @@ class EmbedsMany extends EmbedsOneOrMany {
}
/**
* Simulate order by method.
* Save a new model and attach it to the parent model.
*
* @param string $column
* @param string $direction
* @return Illuminate\Database\Eloquent\Collection
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
public function orderBy($column, $direction = 'asc')
public function performInsert(Model $model, array $values)
{
// Generate a new key if needed.
if ($model->getKeyName() == '_id' and ! $model->getKey())
{
$model->setAttribute('_id', new MongoId);
}
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested())
{
$descending = strtolower($direction) == 'desc';
$this->associate($model);
return $this->getResults()->sortBy($column, SORT_REGULAR, $descending);
return $this->parent->save();
}
// Push the new model to the database.
$result = $this->getBaseQuery()->push($this->localKey, $model->getAttributes(), true);
// Attach the model to its parent.
if ($result) $this->associate($model);
return $result ? $model : false;
}
/**
* Associate the model instance to the given parent, without saving it to the database.
* Save an existing model and attach it to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
* @return Model|bool
*/
public function associate(Model $model)
{
if ( ! $this->contains($model))
public function performUpdate(Model $model, array $values)
{
return $this->associateNew($model);
}
else
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested())
{
return $this->associateExisting($model);
$this->associate($model);
return $this->parent->save();
}
// Get the correct foreign key value.
$foreignKey = $this->getForeignKeyValue($model);
// Use array dot notation for better update behavior.
$values = array_dot($model->getDirty(), $this->localKey . '.$.');
// Update document in database.
$result = $this->getBaseQuery()->where($this->localKey . '.' . $model->getKeyName(), $foreignKey)
->update($values);
// Attach the model to its parent.
if ($result) $this->associate($model);
return $result ? $model : false;
}
/**
* Destroy the embedded models for the given IDs.
* Delete an existing model and detach it from the parent model.
*
* @param mixed $ids
* @param Model $model
* @return int
*/
public function destroy($ids = array())
public function performDelete(Model $model)
{
$ids = $this->getIdsArrayFrom($ids);
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested())
{
$this->dissociate($model);
// Get all models matching the given ids.
$models = $this->get()->only($ids);
return $this->parent->save();
}
// Pull the documents from the database.
foreach ($models as $model)
// Get the correct foreign key value.
$foreignKey = $this->getForeignKeyValue($model);
$result = $this->getBaseQuery()->pull($this->localKey, array($model->getKeyName() => $foreignKey));
if ($result) $this->dissociate($model);
return $result;
}
/**
* 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)
{
$this->performDelete($model);
if ( ! $this->contains($model))
{
return $this->associateNew($model);
}
else
{
return $this->associateExisting($model);
}
}
......@@ -81,6 +135,7 @@ class EmbedsMany extends EmbedsOneOrMany {
$ids = $this->getIdsArrayFrom($ids);
$records = $this->getEmbedded();
$primaryKey = $this->related->getKeyName();
// Remove the document from the parent model.
......@@ -101,27 +156,41 @@ class EmbedsMany extends EmbedsOneOrMany {
}
/**
* Delete all embedded models.
* Destroy the embedded models for the given IDs.
*
* @param mixed $ids
* @return int
*/
public function delete()
public function destroy($ids = array())
{
// Overwrite the local key with an empty array.
$result = $this->query->update(array($this->localKey => array()));
$count = 0;
// If the update query was successful, we will remove the embedded records
// of the parent instance.
if ($result)
{
$count = $this->count();
$ids = $this->getIdsArrayFrom($ids);
$this->setEmbedded(array());
// Get all models matching the given ids.
$models = $this->getResults()->only($ids);
// Pull the documents from the database.
foreach ($models as $model)
{
if ($model->delete()) $count++;
}
// Return the number of deleted embedded records.
return $count;
}
/**
* Delete all embedded models.
*
* @return int
*/
public function delete()
{
// Overwrite the local key with an empty array.
$result = $this->query->update(array($this->localKey => array()));
if ($result) $this->setEmbedded(array());
return $result;
}
......@@ -147,78 +216,6 @@ class EmbedsMany extends EmbedsOneOrMany {
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)
{
// Create a new key if needed.
if ( ! $model->getAttribute('_id'))
{
$model->setAttribute('_id', new MongoId);
}
// Push the new model to the database.
$result = $this->query->push($this->localKey, $model->getAttributes(), true);
// Associate the new model to the parent.
if ($result) $this->associateNew($model);
return $result ? $model : 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)
{
// 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()));
// Update the related model in the parent instance
if ($result) $this->associateExisting($model);
return $result ? $model : 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.
*
......@@ -235,12 +232,10 @@ class EmbedsMany extends EmbedsOneOrMany {
$records = $this->getEmbedded();
// Add the document to the parent model.
// Add the new model to the embedded documents.
$records[] = $model->getAttributes();
$this->setEmbedded($records);
return $model;
return $this->setEmbedded($records);
}
/**
......@@ -255,6 +250,7 @@ class EmbedsMany extends EmbedsOneOrMany {
$records = $this->getEmbedded();
$primaryKey = $this->related->getKeyName();
$key = $model->getKey();
// Replace the document in the parent model.
......@@ -267,9 +263,29 @@ class EmbedsMany extends EmbedsOneOrMany {
}
}
$this->setEmbedded($records);
return $this->setEmbedded($records);
}
/**
* Get a paginator for the "select" statement.
*
* @param int $perPage
* @param array $columns
* @return \Illuminate\Pagination\Paginator
*/
public function paginate($perPage = null, $columns = array('*'))
{
$perPage = $perPage ?: $this->related->getPerPage();
$paginator = $this->related->getConnection()->getPaginator();
$results = $this->getEmbedded();
$start = ($paginator->getCurrentPage() - 1) * $perPage;
$sliced = array_slice($results, $start, $perPage);
return $model;
return $paginator->make($sliced, count($results), $perPage);
}
/**
......@@ -290,7 +306,7 @@ class EmbedsMany extends EmbedsOneOrMany {
*/
protected function setEmbedded($models)
{
if (! is_array($models)) $models = array($models);
if ( ! is_array($models)) $models = array($models);
return parent::setEmbedded(array_values($models));
}
......@@ -305,7 +321,7 @@ class EmbedsMany extends EmbedsOneOrMany {
public function __call($method, $parameters)
{
// Collection methods
if (method_exists('Illuminate\Database\Eloquent\Collection', $method))
if (method_exists('Jenssegers\Mongodb\Eloquent\Collection', $method))
{
return call_user_func_array(array($this->getResults(), $method), $parameters);
}
......
<?php namespace Jenssegers\Mongodb\Relations;
use MongoId;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use MongoId;
class EmbedsOne extends EmbedsOneOrMany {
/**
* Get the results of the relationship.
*
* @return Illuminate\Database\Eloquent\Collection
* @return \Illuminate\Database\Eloquent\Model
*/
public function getResults()
{
......@@ -19,75 +18,105 @@ class EmbedsOne extends EmbedsOneOrMany {
}
/**
* Check if a model is already embedded.
* Save a new model and attach it to the parent model.
*
* @param mixed $key
* @return bool
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
public function contains($key)
public function performInsert(Model $model, array $values)
{
// Generate a new key if needed.
if ($model->getKeyName() == '_id' and ! $model->getKey())
{
if ($key instanceof Model) $key = $key->getKey();
$model->setAttribute('_id', new MongoId);
}
$embedded = $this->getEmbedded();
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested())
{
$this->associate($model);
$primaryKey = $this->related->getKeyName();
return $this->parent->save();
}
$result = $this->getBaseQuery()->update(array($this->localKey => $model->getAttributes()));
// Attach the model to its parent.
if ($result) $this->associate($model);
return ($embedded and $embedded[$primaryKey] == $key);
return $result ? $model : false;
}
/**
* Associate the model instance to the given parent, without saving it to the database.
* Save an existing model and attach it to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
* @return Model|bool
*/
public function associate(Model $model)
public function performUpdate(Model $model, array $values)
{
// Create a new key if needed.
if ( ! $model->getAttribute('_id'))
if ($this->isNested())
{
$model->setAttribute('_id', new MongoId);
$this->associate($model);
return $this->parent->save();
}
$this->setEmbedded($model->getAttributes());
// Use array dot notation for better update behavior.
$values = array_dot($model->getDirty(), $this->localKey . '.');
return $model;
$result = $this->getBaseQuery()->update($values);
// Attach the model to its parent.
if ($result) $this->associate($model);
return $result ? $model : false;
}
/**
* Save a new model and attach it to the parent model.
* Delete an existing model and detach it from the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
* @param Model $model
* @return int
*/
protected function performInsert(Model $model)
public function performDelete(Model $model)
{
// Create a new key if needed.
if ( ! $model->getAttribute('_id'))
// For deeply nested documents, let the parent handle the changes.
if ($this->isNested())
{
$model->setAttribute('_id', new MongoId);
$this->dissociate($model);
return $this->parent->save();
}
$result = $this->query->update(array($this->localKey => $model->getAttributes()));
// Overwrite the local key with an empty array.
$result = $this->getBaseQuery()->update(array($this->localKey => null));
if ($result) $this->associate($model);
// Detach the model from its parent.
if ($result) $this->dissociate();
return $result ? $model : false;
return $result;
}
/**
* Save an existing model and attach it to the parent model.
* Attach the model to its parent.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return Model|bool
* @return \Illuminate\Database\Eloquent\Model
*/
protected function performUpdate(Model $model)
public function associate(Model $model)
{
$result = $this->query->update(array($this->localKey => $model->getAttributes()));
if ($result) $this->associate($model);
return $this->setEmbedded($model->getAttributes());
}
return $result ? $model : false;
/**
* Detach the model from its parent.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function dissociate()
{
return $this->setEmbedded(null);
}
/**
......@@ -97,22 +126,9 @@ class EmbedsOne extends EmbedsOneOrMany {
*/
public function delete()
{
// Overwrite the local key with an empty array.
$result = $this->query->update(array($this->localKey => null));
$model = $this->getResults();
// If the update query was successful, we will remove the embedded records
// of the parent instance.
if ($result)
{
$count = $this->count();
$this->setEmbedded(null);
// Return the number of deleted embedded records.
return $count;
}
return $result;
return $this->performDelete($model);
}
}
<?php namespace Jenssegers\Mongodb\Relations;
use MongoId;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use MongoId;
use Illuminate\Database\Eloquent\Collection as BaseCollection;
use Jenssegers\Mongodb\Eloquent\Collection;
abstract class EmbedsOneOrMany extends Relation {
......@@ -48,6 +49,12 @@ abstract class EmbedsOneOrMany extends Relation {
$this->foreignKey = $foreignKey;
$this->relation = $relation;
// If this is a nested relation, we need to get the parent query instead.
if ($parentRelation = $this->getParentRelation())
{
$this->query = $parentRelation->getQuery();
}
$this->addConstraints();
}
......@@ -60,7 +67,7 @@ abstract class EmbedsOneOrMany extends Relation {
{
if (static::$constraints)
{
$this->query->where($this->parent->getKeyName(), '=', $this->parent->getKey());
$this->query->where($this->getQualifiedParentKeyName(), '=', $this->getParentKey());
}
}
......@@ -86,6 +93,8 @@ abstract class EmbedsOneOrMany extends Relation {
{
foreach ($models as $model)
{
$model->setParentRelation($this);
$model->setRelation($relation, $this->related->newCollection());
}
......@@ -100,12 +109,14 @@ abstract class EmbedsOneOrMany extends Relation {
* @param string $relation
* @return array
*/
public function match(array $models, Collection $results, $relation)
public function match(array $models, BaseCollection $results, $relation)
{
foreach ($models as $model)
{
$results = $model->$relation()->getResults();
$model->setParentRelation($this);
$model->setRelation($relation, $results);
}
......@@ -115,7 +126,7 @@ abstract class EmbedsOneOrMany extends Relation {
/**
* Shorthand to get the results of the relationship.
*
* @return Illuminate\Database\Eloquent\Collection
* @return Jenssegers\Mongodb\Eloquent\Collection
*/
public function get()
{
......@@ -140,44 +151,22 @@ abstract class EmbedsOneOrMany extends Relation {
*/
public function save(Model $model)
{
if ($this->fireModelEvent($model, 'saving') === false) return false;
$this->updateTimestamps($model);
// Attach a new model.
if ( ! $this->contains($model))
{
if ($this->fireModelEvent($model, 'creating') === false) return false;
$result = $this->performInsert($model);
$model->setParentRelation($this);
if ($result)
{
$this->fireModelEvent($model, 'created', false);
// Mark model as existing
$model->exists = true;
}
return $model->save() ? $model : false;
}
// Update an existing model.
else
{
if ($this->fireModelEvent($model, 'updating') === false) return false;
$result = $this->performUpdate($model);
if ($result) $this->fireModelEvent($model, 'updated', false);
}
if ($result)
/**
* Attach an array of models to the parent instance.
*
* @param array $models
* @return array
*/
public function saveMany(array $models)
{
$this->fireModelEvent($result, 'saved', false);
return $result;
}
array_walk($models, array($this, 'save'));
return false;
return $models;
}
/**
......@@ -191,24 +180,13 @@ abstract class EmbedsOneOrMany extends Relation {
// 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);
$instance = $this->related->newInstance($attributes);
return $this->save($instance);
}
$instance->setParentRelation($this);
/**
* Attach an array of models to the parent instance.
*
* @param array $models
* @return array
*/
public function saveMany(array $models)
{
array_walk($models, array($this, 'save'));
$instance->save();
return $models;
return $instance;
}
/**
......@@ -219,7 +197,12 @@ abstract class EmbedsOneOrMany extends Relation {
*/
public function createMany(array $records)
{
$instances = array_map(array($this, 'create'), $records);
$instances = array();
foreach ($records as $record)
{
$instances[] = $this->create($record);
}
return $instances;
}
......@@ -232,7 +215,7 @@ abstract class EmbedsOneOrMany extends Relation {
*/
protected function getIdsArrayFrom($ids)
{
if (! is_array($ids)) $ids = array($ids);
if ( ! is_array($ids)) $ids = array($ids);
foreach ($ids as &$id)
{
......@@ -242,26 +225,6 @@ abstract class EmbedsOneOrMany extends Relation {
return $ids;
}
/**
* Create a related model instanced.
*
* @param array $attributes [description]
* @return [type] [description]
*/
protected function toModel($attributes = array())
{
if (is_null($attributes)) return null;
$model = $this->related->newFromBuilder((array) $attributes);
// Attatch the parent relation to the embedded model.
$model->setRelation($this->foreignKey, $this->parent);
$model->setHidden(array_merge($model->getHidden(), array($this->foreignKey)));
return $model;
}
/**
* Get the embedded records array.
*
......@@ -278,43 +241,20 @@ abstract class EmbedsOneOrMany extends Relation {
/**
* Set the embedded records array.
*
* @param array $models
* @return void
* @param array $records
* @return \Illuminate\Database\Eloquent\Model
*/
protected function setEmbedded($data)
protected function setEmbedded($records)
{
$attributes = $this->parent->getAttributes();
$attributes[$this->localKey] = $data;
$attributes[$this->localKey] = $records;
// Set raw attributes to skip mutators.
$this->parent->setRawAttributes($attributes);
// Set the relation on the parent.
$this->parent->setRelation($this->relation, $this->getResults());
}
/**
* Update the creation and update timestamps.
*
* @return void
*/
protected function updateTimestamps(Model $model)
{
// Check if this model uses timestamps first.
if ( ! $model->timestamps) return;
$time = $model->freshTimestamp();
if ( ! $model->isDirty(Model::UPDATED_AT))
{
$model->setUpdatedAt($time);
}
if ( ! $model->exists && ! $model->isDirty(Model::CREATED_AT))
{
$model->setCreatedAt($time);
}
return $this->parent->setRelation($this->relation, $this->getResults());
}
/**
......@@ -338,13 +278,12 @@ abstract class EmbedsOneOrMany extends Relation {
* Convert an array of records to a Collection.
*
* @param array $records
* @return Illuminate\Database\Eloquent\Collection
* @return Jenssegers\Mongodb\Eloquent\Collection
*/
protected function toCollection(array $records = array())
{
$models = array();
// Wrap records in model objects.
foreach ($records as $attributes)
{
$models[] = $this->toModel($attributes);
......@@ -355,30 +294,113 @@ abstract class EmbedsOneOrMany extends Relation {
$models = $this->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
return new Collection($models);
}
/**
* Fire the given event for the given model.
* Create a related model instanced.
*
* @param string $event
* @param bool $halt
* @return mixed
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model
*/
protected function fireModelEvent(Model $model, $event, $halt = true)
protected function toModel($attributes = array())
{
$dispatcher = $model->getEventDispatcher();
if (is_null($attributes)) return null;
if ( is_null($dispatcher)) return true;
$model = $this->related->newFromBuilder((array) $attributes);
// We will append the names of the class to the event to distinguish it from
// other model events that are fired, allowing us to listen on each model
// event set individually instead of catching event for all the models.
$event = "eloquent.{$event}: ".get_class($model);
$model->setParentRelation($this);
$method = $halt ? 'until' : 'fire';
$model->setRelation($this->foreignKey, $this->parent);
// If you remove this, you will get segmentation faults!
$model->setHidden(array_merge($model->getHidden(), array($this->foreignKey)));
return $model;
}
/**
* Get the relation instance of the parent.
*
* @return Illuminate\Database\Eloquent\Relations\Relation
*/
protected function getParentRelation()
{
return $this->parent->getParentRelation();
}
/**
* Get the underlying query for the relation.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getQuery()
{
// Because we are sharing this relation instance to models, we need
// to make sure we use separate query instances.
return clone $this->query;
}
return $dispatcher->$method($event, $model);
/**
* Get the base query builder driving the Eloquent builder.
*
* @return \Illuminate\Database\Query\Builder
*/
public function getBaseQuery()
{
// Because we are sharing this relation instance to models, we need
// to make sure we use separate query instances.
return clone $this->query->getQuery();
}
/**
* Check if this relation is nested in another relation.
*
* @return boolean
*/
protected function isNested()
{
return $this->getParentRelation() != null;
}
/**
* Get the fully qualified local key name.
*
* @return string
*/
protected function getPathHierarchy($glue = '.')
{
if ($parentRelation = $this->getParentRelation())
{
return $parentRelation->getPathHierarchy($glue) . $glue . $this->localKey;
}
return $this->localKey;
}
/**
* Get the parent's fully qualified key name.
*
* @return string
*/
protected function getQualifiedParentKeyName()
{
if ($parentRelation = $this->getParentRelation())
{
return $parentRelation->getPathHierarchy() . '.' . $this->parent->getKeyName();
}
return $this->parent->getKeyName();
}
/**
* Get the primary key value of the parent.
*
* @return string
*/
protected function getParentKey()
{
return $this->parent->getKey();
}
}
......@@ -20,4 +20,14 @@ class HasMany extends EloquentHasMany {
return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true);
}
/**
* Get the plain foreign key.
*
* @return string
*/
public function getPlainForeignKey()
{
return $this->getForeignKey();
}
}
......@@ -20,4 +20,14 @@ class HasOne extends EloquentHasOne {
return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true);
}
/**
* Get the plain foreign key.
*
* @return string
*/
public function getPlainForeignKey()
{
return $this->getForeignKey();
}
}
......@@ -36,6 +36,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
public function __construct(Connection $connection, $collection)
{
$this->connection = $connection;
$this->collection = $connection->getCollection($collection);
}
......@@ -50,11 +51,12 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
{
$columns = $this->fluent($columns);
// Columns are passed as a default array
// Columns are passed as a default array.
if (is_array($columns) && is_int(key($columns)))
{
// Transform the columns to the required array format
// Transform the columns to the required array format.
$transform = array();
foreach ($columns as $column)
{
$transform[$column] = 1;
......@@ -78,11 +80,12 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
{
$columns = $this->fluent($columns);
// Columns are passed as a default array
// Columns are passed as a default array.
if (is_array($columns) && is_int(key($columns)))
{
// Transform the columns to the required array format
// Transform the columns to the required array format.
$transform = array();
foreach ($columns as $column)
{
$transform[$column] = 1;
......@@ -105,6 +108,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
public function unique($columns = null, $name = null)
{
$columns = $this->fluent($columns);
$this->index($columns, array('unique' => true));
return $this;
......@@ -119,6 +123,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
public function background($columns = null)
{
$columns = $this->fluent($columns);
$this->index($columns, array('background' => true));
return $this;
......@@ -149,6 +154,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
public function expire($columns, $seconds)
{
$columns = $this->fluent($columns);
$this->index($columns, array('expireAfterSeconds' => $seconds));
return $this;
......@@ -163,8 +169,9 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
{
$collection = $this->collection->getName();
// Ensure the collection is created
$db = $this->connection->getMongoDB();
// Ensure the collection is created.
$db->createCollection($collection);
}
......@@ -189,6 +196,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
protected function addColumn($type, $name, array $parameters = array())
{
$this->fluent($name);
return $this;
}
......@@ -221,6 +229,7 @@ class Blueprint extends \Illuminate\Database\Schema\Blueprint {
*/
public function __call($method, $args)
{
// Dummy.
return $this;
}
......
<?php
class AuthTest extends TestCase {
public function tearDown()
{
User::truncate();
DB::collection('password_reminders')->truncate();
}
public function testAuthAttempt()
{
$user = User::create(array(
'name' => 'John Doe',
'email' => 'john@doe.com',
'password' => Hash::make('foobar')
));
$this->assertTrue(Auth::attempt(array('email' => 'john@doe.com', 'password' => 'foobar'), true));
$this->assertTrue(Auth::check());
}
public function testRemind()
{
$mailer = Mockery::mock('Illuminate\Mail\Mailer');
$this->app->instance('mailer', $mailer);
$user = User::create(array(
'name' => 'John Doe',
'email' => 'john@doe.com',
'password' => Hash::make('foobar')
));
$mailer->shouldReceive('send')->once();
Password::remind(array('email' => 'john@doe.com'));
$this->assertEquals(1, DB::collection('password_reminders')->count());
$reminder = DB::collection('password_reminders')->first();
$this->assertEquals('john@doe.com', $reminder['email']);
$this->assertNotNull($reminder['token']);
$this->assertInstanceOf('MongoDate', $reminder['created_at']);
$credentials = array(
'email' => 'john@doe.com',
'password' => 'foobar',
'password_confirmation' => 'foobar',
'token' => $reminder['token']
);
$response = Password::reset($credentials, function($user, $password)
{
$user->password = Hash::make($password);
$user->save();
});
$this->assertEquals('reminders.reset', $response);
$this->assertEquals(0, DB::collection('password_reminders')->count());
}
public function testDeprecatedRemind()
{
$mailer = Mockery::mock('Illuminate\Mail\Mailer');
$this->app->instance('mailer', $mailer);
$user = User::create(array(
'name' => 'John Doe',
'email' => 'john@doe.com',
'password' => Hash::make('foobar')
));
$mailer->shouldReceive('send')->once();
Password::remind(array('email' => 'john@doe.com'));
DB::collection('password_reminders')->update(array('created_at' => new DateTime));
$reminder = DB::collection('password_reminders')->first();
$this->assertTrue(is_array($reminder['created_at']));
$credentials = array(
'email' => 'john@doe.com',
'password' => 'foobar',
'password_confirmation' => 'foobar',
'token' => $reminder['token']
);
$response = Password::reset($credentials, function($user, $password)
{
$user->password = Hash::make($password);
$user->save();
});
$this->assertEquals('reminders.reset', $response);
$this->assertEquals(0, DB::collection('password_reminders')->count());
}
}
......@@ -29,7 +29,8 @@ class EmbeddedRelationsTest extends TestCase {
$address = $user->addresses()->save($address);
$address->unsetEventDispatcher();
$this->assertNotNull($user->_addresses);
$this->assertNotNull($user->addresses);
$this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $user->addresses);
$this->assertEquals(array('London'), $user->addresses->lists('city'));
$this->assertInstanceOf('DateTime', $address->created_at);
$this->assertInstanceOf('DateTime', $address->updated_at);
......@@ -73,6 +74,7 @@ class EmbeddedRelationsTest extends TestCase {
$user = User::find($user->_id);
$user->addresses()->save(new Address(array('city' => 'Bruxelles')));
$this->assertEquals(array('London', 'New York', 'Bruxelles'), $user->addresses->lists('city'));
$address = $user->addresses[1];
$address->city = "Manhattan";
$user->addresses()->save($address);
......@@ -82,6 +84,29 @@ class EmbeddedRelationsTest extends TestCase {
$this->assertEquals(array('London', 'Manhattan', 'Bruxelles'), $freshUser->addresses->lists('city'));
}
public function testEmbedsManySaveModel()
{
$user = User::create(array('name' => 'John Doe'));
$address = new Address(array('city' => 'London'));
$address->setEventDispatcher($events = Mockery::mock('Illuminate\Events\Dispatcher'));
$events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($address), $address)->andReturn(true);
$events->shouldReceive('until')->once()->with('eloquent.creating: '.get_class($address), $address)->andReturn(true);
$events->shouldReceive('fire')->once()->with('eloquent.created: '.get_class($address), $address);
$events->shouldReceive('fire')->once()->with('eloquent.saved: '.get_class($address), $address);
$address->save();
$address->setEventDispatcher($events = Mockery::mock('Illuminate\Events\Dispatcher'));
$events->shouldReceive('until')->once()->with('eloquent.saving: '.get_class($address), $address)->andReturn(true);
$events->shouldReceive('until')->once()->with('eloquent.updating: '.get_class($address), $address)->andReturn(true);
$events->shouldReceive('fire')->once()->with('eloquent.updated: '.get_class($address), $address);
$events->shouldReceive('fire')->once()->with('eloquent.saved: '.get_class($address), $address);
$address->city = 'Paris';
$address->save();
}
public function testEmbedsToArray()
{
$user = User::create(array('name' => 'John Doe'));
......@@ -97,8 +122,7 @@ class EmbeddedRelationsTest extends TestCase {
$user = User::create(array('name' => 'John Doe'));
$address = new Address(array('city' => 'London'));
$address = $user->addresses()->associate($address);
$this->assertNotNull($user->_addresses);
$user->addresses()->associate($address);
$this->assertEquals(array('London'), $user->addresses->lists('city'));
$this->assertNotNull($address->_id);
......@@ -187,8 +211,8 @@ class EmbeddedRelationsTest extends TestCase {
$address = $user->addresses->first();
$address->setEventDispatcher($events = Mockery::mock('Illuminate\Events\Dispatcher'));
$events->shouldReceive('until')->once()->with('eloquent.deleting: '.get_class($address), Mockery::mustBe($address))->andReturn(true);
$events->shouldReceive('fire')->once()->with('eloquent.deleted: '.get_class($address), Mockery::mustBe($address));
$events->shouldReceive('until')->once()->with('eloquent.deleting: '.get_class($address), Mockery::type('Address'))->andReturn(true);
$events->shouldReceive('fire')->once()->with('eloquent.deleted: '.get_class($address), Mockery::type('Address'));
$user->addresses()->destroy($address->_id);
$this->assertEquals(array('Bristol', 'Bruxelles'), $user->addresses->lists('city'));
......@@ -217,6 +241,32 @@ class EmbeddedRelationsTest extends TestCase {
$this->assertEquals(array('Bristol'), $user->addresses->lists('city'));
}
public function testEmbedsManyDelete()
{
$user = User::create(array('name' => 'John Doe'));
$user->addresses()->saveMany(array(new Address(array('city' => 'London')), new Address(array('city' => 'Bristol')), new Address(array('city' => 'Bruxelles'))));
$address = $user->addresses->first();
$address->setEventDispatcher($events = Mockery::mock('Illuminate\Events\Dispatcher'));
$events->shouldReceive('until')->once()->with('eloquent.deleting: '.get_class($address), Mockery::type('Address'))->andReturn(true);
$events->shouldReceive('fire')->once()->with('eloquent.deleted: '.get_class($address), Mockery::type('Address'));
$address->delete();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(2, $user->addresses->count());
$address->unsetEventDispatcher();
$address = $user->addresses->first();
$address->delete();
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(1, $user->addresses()->count());
$this->assertEquals(1, $user->addresses->count());
}
public function testEmbedsManyDissociate()
{
$user = User::create(array());
......@@ -328,16 +378,18 @@ class EmbeddedRelationsTest extends TestCase {
$user = User::find($user1->id);
$relations = $user->getRelations();
$this->assertFalse(array_key_exists('addresses', $relations));
$this->assertArrayNotHasKey('addresses', $user->toArray());
$this->assertArrayHasKey('addresses', $user->toArray());
$this->assertTrue(is_array($user->toArray()['addresses']));
$user = User::with('addresses')->get()->first();
$relations = $user->getRelations();
$this->assertTrue(array_key_exists('addresses', $relations));
$this->assertEquals(2, $relations['addresses']->count());
$this->assertArrayHasKey('addresses', $user->toArray());
$this->assertTrue(is_array($user->toArray()['addresses']));
}
public function testEmbedsManyDelete()
public function testEmbedsManyDeleteAll()
{
$user1 = User::create(array('name' => 'John Doe'));
$user1->addresses()->save(new Address(array('city' => 'New York')));
......@@ -364,14 +416,77 @@ class EmbeddedRelationsTest extends TestCase {
public function testEmbedsManyCollectionMethods()
{
$user = User::create(array('name' => 'John Doe'));
$user->addresses()->save(new Address(array('city' => 'New York')));
$user->addresses()->save(new Address(array('city' => 'Paris')));
$user->addresses()->save(new Address(array('city' => 'Brussels')));
$user->addresses()->save(new Address(array('city' => 'Paris', 'country' => 'France', 'visited' => 4, 'created_at' => new DateTime('3 days ago'))));
$user->addresses()->save(new Address(array('city' => 'Bruges', 'country' => 'Belgium', 'visited' => 7, 'created_at' => new DateTime('5 days ago'))));
$user->addresses()->save(new Address(array('city' => 'Brussels', 'country' => 'Belgium', 'visited' => 2, 'created_at' => new DateTime('4 days ago'))));
$user->addresses()->save(new Address(array('city' => 'Ghent', 'country' => 'Belgium', 'visited' => 13, 'created_at' => new DateTime('2 days ago'))));
$this->assertEquals(array('Paris', 'Bruges', 'Brussels', 'Ghent'), $user->addresses()->lists('city'));
$this->assertEquals(array('Bruges', 'Brussels', 'Ghent', 'Paris'), $user->addresses()->sortBy('city')->lists('city'));
$this->assertEquals(array('Bruges', 'Brussels', 'Ghent', 'Paris'), $user->addresses()->orderBy('city')->lists('city'));
$this->assertEquals(array('Paris', 'Ghent', 'Brussels', 'Bruges'), $user->addresses()->orderBy('city', 'desc')->lists('city'));
$this->assertEquals(array(), $user->addresses()->where('city', 'New York')->lists('city'));
$this->assertEquals(array('Bruges', 'Brussels', 'Ghent'), $user->addresses()->where('country', 'Belgium')->lists('city'));
$this->assertEquals(array('Ghent', 'Brussels', 'Bruges'), $user->addresses()->where('country', 'Belgium')->orderBy('city', 'desc')->lists('city'));
$results = $user->addresses->get(0);
$this->assertInstanceOf('Address', $results);
$results = $user->addresses()->where('country', 'Belgium')->get();
$this->assertInstanceOf('Jenssegers\Mongodb\Eloquent\Collection', $results);
$this->assertEquals(3, $results->count());
$results = $user->addresses()->where('country', '!=', 'Belgium')->get();
$this->assertEquals(1, $results->count());
$results = $user->addresses()->where('visited', '>', 4)->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->where('visited', '<', 7)->get();
$this->assertEquals(2, $results->count());
$this->assertEquals(array('New York', 'Paris', 'Brussels'), $user->addresses()->lists('city'));
$this->assertEquals(array('Brussels', 'New York', 'Paris'), $user->addresses()->sortBy('city')->lists('city'));
$this->assertEquals(array('Brussels', 'New York', 'Paris'), $user->addresses()->orderBy('city')->lists('city'));
$this->assertEquals(array('Paris', 'New York', 'Brussels'), $user->addresses()->orderBy('city', 'desc')->lists('city'));
$results = $user->addresses()->where('visited', '<=', 7)->get();
$this->assertEquals(3, $results->count());
$results = $user->addresses()->where('visited', '>=', 7)->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->where('visited', 'between', array(4, 7))->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->whereBetween('visited', array(4, 7))->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->whereNotBetween('visited', array(4, 7))->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->whereIn('visited', array(7, 13))->get();
$this->assertEquals(2, $results->count());
$results = $user->addresses()->whereNotIn('visited', array(7))->get();
$this->assertEquals(3, $results->count());
$results = $user->addresses()->whereNull('something')->get();
$this->assertEquals(4, $results->count());
$results = $user->addresses()->whereNotNull('visited')->get();
$this->assertEquals(4, $results->count());
$results = $user->addresses()->offset(1)->get();
$this->assertEquals(3, $results->count());
$results = $user->addresses()->skip(1)->get();
$this->assertEquals(3, $results->count());
$results = $user->addresses()->limit(2)->get();
$this->assertEquals(2, $results->count());
$result = $user->addresses()->latest()->first();
$this->assertEquals('Ghent', $result->city);
$result = $user->addresses()->oldest()->first();
$this->assertEquals('Bruges', $result->city);
}
public function testEmbedsOne()
......@@ -388,7 +503,7 @@ class EmbeddedRelationsTest extends TestCase {
$father = $user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->_father);
$this->assertNotNull($user->father);
$this->assertEquals('Mark Doe', $user->father->name);
$this->assertInstanceOf('DateTime', $father->created_at);
$this->assertInstanceOf('DateTime', $father->updated_at);
......@@ -408,7 +523,7 @@ class EmbeddedRelationsTest extends TestCase {
$user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->_father);
$this->assertNotNull($user->father);
$this->assertEquals('Tom Doe', $user->father->name);
$father = new User(array('name' => 'Jim Doe'));
......@@ -422,7 +537,7 @@ class EmbeddedRelationsTest extends TestCase {
$father = $user->father()->save($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->_father);
$this->assertNotNull($user->father);
$this->assertEquals('Jim Doe', $user->father->name);
}
......@@ -437,7 +552,7 @@ class EmbeddedRelationsTest extends TestCase {
$father = $user->father()->associate($father);
$father->unsetEventDispatcher();
$this->assertNotNull($user->_father);
$this->assertNotNull($user->father);
$this->assertEquals('Mark Doe', $user->father->name);
}
......@@ -447,7 +562,6 @@ class EmbeddedRelationsTest extends TestCase {
$father = $user->father()->save(new User(array('name' => 'Mark Doe')));
$user->father()->delete();
$this->assertNull($user->_father);
$this->assertNull($user->father);
}
......@@ -459,11 +573,180 @@ class EmbeddedRelationsTest extends TestCase {
$user->addresses()->save(new Address(array('city' => 'Brussels')));
$array = $user->toArray();
$this->assertArrayNotHasKey('_addresses', $array);
$this->assertArrayHasKey('addresses', $array);
$this->assertTrue(is_array($array['addresses']));
}
$user->setExposed(array('_addresses'));
$array = $user->toArray();
$this->assertArrayHasKey('_addresses', $array);
public function testEmbeddedSave()
{
$user = User::create(array('name' => 'John Doe'));
$address = $user->addresses()->create(array('city' => 'New York'));
$father = $user->father()->create(array('name' => 'Mark Doe'));
$address->city = 'Paris';
$address->save();
$father->name = 'Steve Doe';
$father->save();
$this->assertEquals('Paris', $user->addresses->first()->city);
$this->assertEquals('Steve Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('Paris', $user->addresses->first()->city);
$this->assertEquals('Steve Doe', $user->father->name);
$address = $user->addresses()->first();
$father = $user->father;
$address->city = 'Ghent';
$address->save();
$father->name = 'Mark Doe';
$father->save();
$this->assertEquals('Ghent', $user->addresses->first()->city);
$this->assertEquals('Mark Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('Ghent', $user->addresses->first()->city);
$this->assertEquals('Mark Doe', $user->father->name);
}
public function testNestedEmbedsOne()
{
$user = User::create(array('name' => 'John Doe'));
$father = $user->father()->create(array('name' => 'Mark Doe'));
$grandfather = $father->father()->create(array('name' => 'Steve Doe'));
$greatgrandfather = $grandfather->father()->create(array('name' => 'Tom Doe'));
$user->name = 'Tim Doe';
$user->save();
$father->name = 'Sven Doe';
$father->save();
$greatgrandfather->name = 'Ron Doe';
$greatgrandfather->save();
$this->assertEquals('Tim Doe', $user->name);
$this->assertEquals('Sven Doe', $user->father->name);
$this->assertEquals('Steve Doe', $user->father->father->name);
$this->assertEquals('Ron Doe', $user->father->father->father->name);
$user = User::where('name', 'Tim Doe')->first();
$this->assertEquals('Tim Doe', $user->name);
$this->assertEquals('Sven Doe', $user->father->name);
$this->assertEquals('Steve Doe', $user->father->father->name);
$this->assertEquals('Ron Doe', $user->father->father->father->name);
}
public function testNestedEmbedsMany()
{
$user = User::create(array('name' => 'John Doe'));
$country1 = $user->addresses()->create(array('country' => 'France'));
$country2 = $user->addresses()->create(array('country' => 'Belgium'));
$city1 = $country1->addresses()->create(array('city' => 'Paris'));
$city2 = $country2->addresses()->create(array('city' => 'Ghent'));
$city3 = $country2->addresses()->create(array('city' => 'Brussels'));
$city3->city = 'Bruges';
$city3->save();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(1, $user->addresses()->first()->addresses()->count());
$this->assertEquals(2, $user->addresses()->last()->addresses()->count());
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(2, $user->addresses()->count());
$this->assertEquals(1, $user->addresses()->first()->addresses()->count());
$this->assertEquals(2, $user->addresses()->last()->addresses()->count());
}
public function testNestedMixedEmbeds()
{
$user = User::create(array('name' => 'John Doe'));
$father = $user->father()->create(array('name' => 'Mark Doe'));
$country1 = $father->addresses()->create(array('country' => 'France'));
$country2 = $father->addresses()->create(array('country' => 'Belgium'));
$country2->country = 'England';
$country2->save();
$father->name = 'Steve Doe';
$father->save();
$this->assertEquals('France', $user->father->addresses()->first()->country);
$this->assertEquals('England', $user->father->addresses()->last()->country);
$this->assertEquals('Steve Doe', $user->father->name);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals('France', $user->father->addresses()->first()->country);
$this->assertEquals('England', $user->father->addresses()->last()->country);
$this->assertEquals('Steve Doe', $user->father->name);
}
public function testDoubleAssociate()
{
$user = User::create(array('name' => 'John Doe'));
$address = new Address(array('city' => 'Paris'));
$user->addresses()->associate($address);
$user->addresses()->associate($address);
$address = $user->addresses()->first();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
$user = User::where('name', 'John Doe')->first();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
$user->save();
$user->addresses()->associate($address);
$this->assertEquals(1, $user->addresses()->count());
}
public function testSaveEmptyModel()
{
$user = User::create(array('name' => 'John Doe'));
$user->addresses()->save(new Address);
$this->assertNotNull($user->addresses);
$this->assertEquals(1, $user->addresses()->count());
}
public function testIncrementEmbedded()
{
$user = User::create(array('name' => 'John Doe'));
$address = $user->addresses()->create(array('city' => 'New York', 'visited' => 5));
$address->increment('visited');
$this->assertEquals(6, $address->visited);
$this->assertEquals(6, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(6, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$address = $user->addresses()->first();
$address->decrement('visited');
$this->assertEquals(5, $address->visited);
$this->assertEquals(5, $user->addresses()->first()->visited);
$user = User::where('name', 'John Doe')->first();
$this->assertEquals(5, $user->addresses()->first()->visited);
}
public function testPaginateEmbedsMany()
{
$user = User::create(array('name' => 'John Doe'));
$user->addresses()->save(new Address(array('city' => 'New York')));
$user->addresses()->save(new Address(array('city' => 'Paris')));
$user->addresses()->save(new Address(array('city' => 'Brussels')));
$results = $user->addresses()->paginate(2);
$this->assertEquals(2, $results->count());
$this->assertEquals(3, $results->getTotal());
}
}
......@@ -267,29 +267,33 @@ class ModelTest extends TestCase {
public function testSoftDelete()
{
$user = new Soft;
$user->name = 'Softy';
$user->save();
Soft::create(array('name' => 'John Doe'));
Soft::create(array('name' => 'Jane Doe'));
$this->assertEquals(2, Soft::count());
$user = Soft::where('name', 'John Doe')->first();
$this->assertEquals(true, $user->exists);
$this->assertEquals(false, $user->trashed());
$this->assertNull($user->deleted_at);
$user->delete();
$this->assertEquals(true, $user->trashed());
$this->assertNotNull($user->deleted_at);
$check = Soft::find($user->_id);
$this->assertEquals(null, $check);
$all = Soft::get();
$this->assertEquals(0, $all->count());
$user = Soft::where('name', 'John Doe')->first();
$this->assertNull($user);
$all = Soft::withTrashed()->get();
$this->assertEquals(1, $all->count());
$this->assertEquals(1, Soft::count());
$this->assertEquals(2, Soft::withTrashed()->count());
$check = $all[0];
$this->assertInstanceOf('Carbon\Carbon', $check->deleted_at);
$this->assertEquals(true, $check->trashed());
$user = Soft::withTrashed()->where('name', 'John Doe')->first();
$this->assertNotNull($user);
$this->assertInstanceOf('Carbon\Carbon', $user->deleted_at);
$this->assertEquals(true, $user->trashed());
$check->restore();
$all = Soft::get();
$this->assertEquals(1, $all->count());
$user->restore();
$this->assertEquals(2, Soft::count());
}
public function testPrimaryKey()
......@@ -393,6 +397,12 @@ class ModelTest extends TestCase {
$user = User::create(array('name' => 'Jane Doe', 'birthday' => '2005-08-08'));
$this->assertInstanceOf('Carbon\Carbon', $user->birthday);
$user = User::create(array('name' => 'Jane Doe', 'entry' => array('date' => '2005-08-08')));
$this->assertInstanceOf('Carbon\Carbon', $user->getAttribute('entry.date'));
$user->setAttribute('entry.date', new DateTime);
$this->assertInstanceOf('Carbon\Carbon', $user->getAttribute('entry.date'));
}
public function testIdAttribute()
......@@ -406,14 +416,28 @@ class ModelTest extends TestCase {
public function testPushPull()
{
$user = User::create(array('name' => 'John Doe', 'tags' => array()));
$user = User::create(array('name' => 'John Doe'));
$result = User::where('_id', $user->_id)->push('tags', 'tag1');
$user->push('tags', 'tag1');
$user->push('tags', array('tag1', 'tag2'));
$user->push('tags', 'tag2', true);
$this->assertEquals(array('tag1', 'tag1', 'tag2'), $user->tags);
$user = User::where('_id', $user->_id)->first();
$this->assertEquals(array('tag1', 'tag1', 'tag2'), $user->tags);
$user->pull('tags', 'tag1');
$this->assertEquals(array('tag2'), $user->tags);
$user = User::where('_id', $user->_id)->first();
$this->assertEquals(array('tag2'), $user->tags);
$user->push('tags', 'tag3');
$user->pull('tags', array('tag2', 'tag3'));
$this->assertTrue(is_int($result));
$this->assertTrue(is_array($user->tags));
$this->assertEquals(1, count($user->tags));
$this->assertEquals(array(), $user->tags);
$user = User::where('_id', $user->_id)->first();
$this->assertEquals(array(), $user->tags);
}
public function testRaw()
......
......@@ -611,4 +611,20 @@ class QueryBuilderTest extends TestCase {
$this->assertEquals(1, $user['age']);
}
public function testProjections()
{
DB::collection('items')->insert(array(
array('name' => 'fork', 'tags' => array('sharp', 'pointy')),
array('name' => 'spork', 'tags' => array('sharp', 'pointy', 'round', 'bowl')),
array('name' => 'spoon', 'tags' => array('round', 'bowl')),
));
$results = DB::collection('items')->project(array('tags' => array('$slice' => 1)))->get();
foreach ($results as $result)
{
$this->assertEquals(1, count($result['tags']));
}
}
}
......@@ -239,6 +239,31 @@ class QueryTest extends TestCase {
->get();
$this->assertEquals(5, count($users));
$users = User::whereNull('deleted_at')
->where('title', 'admin')
->where(function($query)
{
$query->where('age', '>', 15)
->orWhere('name', 'Harry Hoe');
})
->get();
$this->assertEquals(3, $users->count());
$users = User::whereNull('deleted_at')
->where(function($query)
{
$query->where('name', 'Harry Hoe')
->orWhere(function($query)
{
$query->where('age', '>', 15)
->where('title', '<>', 'admin');
});
})
->get();
$this->assertEquals(5, $users->count());
}
public function testWhereRaw()
......@@ -280,4 +305,15 @@ class QueryTest extends TestCase {
$this->assertEquals(2, count($users));
}
public function testPaginate()
{
$results = User::paginate(2);
$this->assertEquals(2, $results->count());
$this->assertNotNull($results->first()->title);
$results = User::paginate(2, array('name', 'age'));
$this->assertEquals(2, $results->count());
$this->assertNull($results->first()->title);
}
}
......@@ -131,6 +131,7 @@ class RelationsTest extends TestCase {
$items = $user->items;
$this->assertEquals(1, count($items));
$this->assertInstanceOf('Item', $items[0]);
$this->assertEquals($user->_id, $items[0]->user_id);
// Has one
$user = User::create(array('name' => 'John Doe'));
......@@ -141,6 +142,7 @@ class RelationsTest extends TestCase {
$role = $user->role;
$this->assertInstanceOf('Role', $role);
$this->assertEquals('admin', $role->type);
$this->assertEquals($user->_id, $role->user_id);
}
public function testBelongsToMany()
......@@ -415,6 +417,59 @@ class RelationsTest extends TestCase {
$address = $client->addresses->first();
$this->assertEquals('Paris', $address->data['city']);
$client = Client::with('addresses')->first();
$this->assertEquals('Paris', $client->addresses->first()->data['city']);
}
public function testDoubleSaveOneToMany()
{
$author = User::create(array('name' => 'George R. R. Martin'));
$book = Book::create(array('title' => 'A Game of Thrones'));
$author->books()->save($book);
$author->books()->save($book);
$author->save();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->_id, $book->author_id);
$author = User::where('name', 'George R. R. Martin')->first();
$book = Book::where('title', 'A Game of Thrones')->first();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->_id, $book->author_id);
$author->books()->save($book);
$author->books()->save($book);
$author->save();
$this->assertEquals(1, $author->books()->count());
$this->assertEquals($author->_id, $book->author_id);
}
public function testDoubleSaveManyToMany()
{
$user = User::create(array('name' => 'John Doe'));
$client = Client::create(array('name' => 'Admins'));
$user->clients()->save($client);
$user->clients()->save($client);
$user->save();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals(array($user->_id), $client->user_ids);
$this->assertEquals(array($client->_id), $user->client_ids);
$user = User::where('name', 'John Doe')->first();
$client = Client::where('name', 'Admins')->first();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals(array($user->_id), $client->user_ids);
$this->assertEquals(array($client->_id), $user->client_ids);
$user->clients()->save($client);
$user->clients()->save($client);
$user->save();
$this->assertEquals(1, $user->clients()->count());
$this->assertEquals(array($user->_id), $client->user_ids);
$this->assertEquals(array($client->_id), $user->client_ids);
}
}
......@@ -9,7 +9,10 @@ class TestCase extends Orchestra\Testbench\TestCase {
*/
protected function getPackageProviders()
{
return array('Jenssegers\Mongodb\MongodbServiceProvider');
return array(
'Jenssegers\Mongodb\MongodbServiceProvider',
'Jenssegers\Mongodb\Auth\ReminderServiceProvider',
);
}
/**
......
......@@ -6,4 +6,9 @@ class Address extends Eloquent {
protected static $unguarded = true;
public function addresses()
{
return $this->embedsMany('Address');
}
}
......@@ -19,6 +19,6 @@ class Client extends Eloquent {
public function addresses()
{
return $this->hasMany('Address', 'data.client_id', 'data.address_id');
return $this->hasMany('Address', 'data.address_id', 'data.client_id');
}
}
......@@ -8,6 +8,7 @@ class Soft extends Eloquent {
use SoftDeletingTrait;
protected $collection = 'soft';
protected static $unguarded = true;
protected $dates = array('deleted_at');
}
......@@ -2,12 +2,16 @@
use Jenssegers\Mongodb\Model as Eloquent;
use Illuminate\Auth\UserTrait;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableTrait;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
protected $dates = array('birthday');
use UserTrait, RemindableTrait;
protected $dates = array('birthday', 'entry.date');
protected static $unguarded = true;
public function books()
......@@ -60,66 +64,6 @@ class User extends Eloquent implements UserInterface, RemindableInterface {
return $this->embedsOne('User');
}
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the e-mail address where password reminders are sent.
*
* @return string
*/
public function getReminderEmail()
{
return $this->email;
}
/**
* Get the token value for the "remember me" session.
*
* @return string
*/
public function getRememberToken()
{
return $this->rememberToken;
}
/**
* Set the token value for the "remember me" session.
*
* @param string $value
* @return void
*/
public function setRememberToken($value)
{
$this->rememberToken = $value;
}
/**
* Get the column name for the "remember me" token.
*
* @return string
*/
public function getRememberTokenName() {
return 'remember_token';
}
protected function getDateFormat()
{
return 'l jS \of F Y h:i:s A';
......
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