Commit f5c230f8 authored by Jens Segers's avatar Jens Segers

Merge master

parents 72c64442 57c0d851
Laravel MongoDB
===============
[![Latest Stable Version](https://poser.pugx.org/jenssegers/mongodb/v/stable.png)](https://packagist.org/packages/jenssegers/mongodb) [![Total Downloads](https://poser.pugx.org/jenssegers/mongodb/downloads.png)](https://packagist.org/packages/jenssegers/mongodb) [![Build Status](https://travis-ci.org/jenssegers/Laravel-MongoDB.png?branch=master)](https://travis-ci.org/jenssegers/Laravel-MongoDB) [![Coverage Status](https://coveralls.io/repos/jenssegers/Laravel-MongoDB/badge.png?branch=master)](https://coveralls.io/r/jenssegers/Laravel-MongoDB?branch=master)
[![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.*
......@@ -136,6 +136,11 @@ If you want to use Laravel's native Auth functionality, register this included s
This service provider will slightly modify the internal DatabaseReminderRepository to add support for MongoDB based password reminders. If you don't use password reminders, you don't have to register this service provider and everything else should work just fine.
Sentry
------
If yo want to use this library with [Sentry](https://cartalyst.com/manual/sentry), then check out https://github.com/jenssegers/Laravel-MongoDB-Sentry
Sessions
--------
......@@ -423,6 +428,33 @@ Again, you may override the conventional local key by passing a second argument
return $this->embedsMany('Book', 'local_key');
### EmbedsOne Relations
There is also an EmbedsOne relation, which works similar to the EmbedsMany relation, but only stores one embedded model.
use Jenssegers\Mongodb\Model as Eloquent;
class Book extends Eloquent {
public function author()
{
return $this->embedsOne('Author');
}
}
Now we can access the book's author through the dynamic property:
$author = Book::first()->author;
Inserting and updating embedded documents works just like the `embedsMany` relation:
$author = new Author(array('name' => 'John Doe'));
$book = Books::first();
$author = $user->author()->save($author);
### MySQL Relations
If you're using a hybrid MongoDB and SQL setup, you're in luck! The model will automatically return a MongoDB- or SQL-relation based on the type of the related model. Of course, if you want this functionality to work both ways, your SQL-models will need to extend `Jenssegers\Eloquent\Model`. Note that this functionality only works for hasOne, hasMany and belongsTo relations.
......
......@@ -5,12 +5,12 @@
"authors": [
{
"name": "Jens Segers",
"email": "hello@jenssegers.be"
"homepage": "http://jenssegers.be"
}
],
"license" : "MIT",
"require": {
"php": ">=5.3.0",
"php": ">=5.4.0",
"illuminate/support": "4.2.x",
"illuminate/database": "4.2.x",
"illuminate/events": "4.2.x"
......@@ -25,5 +25,8 @@
"Jenssegers\\Eloquent": "src/"
}
},
"minimum-stability": "dev"
"suggest": {
"jenssegers/mongodb-session": "Add MongoDB session support to Laravel-MongoDB",
"jenssegers/mongodb-sentry": "Add Sentry support to Laravel-MongoDB"
}
}
......@@ -34,6 +34,7 @@
</testsuite>
<testsuite name="relations">
<directory>tests/RelationsTest.php</directory>
<directory>tests/EmbeddedRelationsTest.php</directory>
</testsuite>
<testsuite name="mysqlrelations">
<directory>tests/RelationsTest.php</directory>
......
<?php namespace Jenssegers\Eloquent;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\Relation;
use Jenssegers\Mongodb\Relations\HasOne;
use Jenssegers\Mongodb\Relations\HasMany;
use Jenssegers\Mongodb\Relations\BelongsTo;
use Jenssegers\Mongodb\Relations\BelongsToMany;
use Jenssegers\Mongodb\Relations\MorphTo;
......@@ -134,12 +134,6 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model {
*/
public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null)
{
// Check if it is a relation with an original model.
if (!is_subclass_of($related, 'Jenssegers\Mongodb\Model'))
{
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relatinoships.
......@@ -150,6 +144,12 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model {
$relation = $caller['function'];
}
// Check if it is a relation with an original model.
if (!is_subclass_of($related, 'Jenssegers\Mongodb\Model'))
{
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
......@@ -227,12 +227,6 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model {
*/
public function belongsToMany($related, $collection = null, $foreignKey = null, $otherKey = null, $relation = null)
{
// Check if it is a relation with an original model.
if (!is_subclass_of($related, 'Jenssegers\Mongodb\Model'))
{
return parent::belongsToMany($related, $collection, $foreignKey, $otherKey, $relation);
}
// If no relationship name was passed, we will pull backtraces to get the
// name of the calling function. We will use that function name as the
// title of this relation since that is a great convention to apply.
......@@ -241,6 +235,12 @@ abstract class Model extends \Illuminate\Database\Eloquent\Model {
$relation = $this->getBelongsToManyCaller();
}
// Check if it is a relation with an original model.
if (!is_subclass_of($related, 'Jenssegers\Mongodb\Model'))
{
return parent::belongsToMany($related, $collection, $foreignKey, $otherKey, $relation);
}
// First, we'll need to determine the foreign key and "other key" for the
// relationship. Once we have determined the keys we'll make the query
// instances as well as the relationship instances we need for this.
......
......@@ -180,7 +180,7 @@ class Connection extends \Illuminate\Database\Connection {
*/
public function getDriverName()
{
return '';
return 'mongodb';
}
/**
......
<?php namespace Jenssegers\Mongodb\Eloquent;
use MongoCursor;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
class Builder extends \Illuminate\Database\Eloquent\Builder {
class Builder extends EloquentBuilder {
/**
* The methods that should be returned from query builder.
......@@ -14,6 +16,56 @@ class Builder extends \Illuminate\Database\Eloquent\Builder {
'count', 'min', 'max', 'avg', 'sum', 'exists', 'push', 'pull'
);
/**
* Add the "has" condition where clause to the query.
*
* @param \Illuminate\Database\Eloquent\Builder $hasQuery
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @param string $operator
* @param int $count
* @param string $boolean
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean)
{
$query = $hasQuery->getQuery();
// Get the number of related objects for each possible parent.
$relationCount = array_count_values($query->lists($relation->getHasCompareKey()));
// Remove unwanted related objects based on the operator and count.
$relationCount = array_filter($relationCount, function($counted) use ($count, $operator)
{
// If we are comparing to 0, we always need all results.
if ($count == 0) return true;
switch ($operator)
{
case '>=':
case '<':
return $counted >= $count;
case '>':
case '<=':
return $counted > $count;
case '=':
case '!=':
return $counted == $count;
}
});
// If the operator is <, <= or !=, we will use whereNotIn.
$not = in_array($operator, array('<', '<=', '!='));
// If we are comparing to 0, we need an additional $not flip.
if ($count == 0) $not = !$not;
// All related ids.
$relatedIds = array_keys($relationCount);
// Add whereIn to the query.
return $this->whereIn($this->model->getKeyName(), $relatedIds, $boolean, $not);
}
/**
* Create a raw database expression.
*
......
......@@ -5,6 +5,7 @@ use Jenssegers\Mongodb\DatabaseManager as Resolver;
use Jenssegers\Mongodb\Eloquent\Builder;
use Jenssegers\Mongodb\Query\Builder as QueryBuilder;
use Jenssegers\Mongodb\Relations\EmbedsMany;
use Jenssegers\Mongodb\Relations\EmbedsOne;
use Carbon\Carbon;
use DateTime;
......@@ -84,6 +85,42 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
return new EmbedsMany($query, $this, $instance, $localKey, $foreignKey, $relation);
}
/**
* Define an embedded one-to-many relationship.
*
* @param string $related
* @param string $collection
* @return \Illuminate\Database\Eloquent\Relations\EmbedsMany
*/
protected function embedsOne($related, $localKey = null, $foreignKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relatinoships.
if (is_null($relation))
{
list(, $caller) = debug_backtrace(false);
$relation = $caller['function'];
}
if (is_null($localKey))
{
$localKey = '_' . $relation;
}
if (is_null($foreignKey))
{
$foreignKey = snake_case(class_basename($this));
}
$query = $this->newQuery();
$instance = new $related;
return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation);
}
/**
* Convert a DateTime to a storable MongoDate object.
*
......@@ -92,19 +129,19 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
*/
public function fromDateTime($value)
{
// Convert DateTime to MongoDate
if ($value instanceof DateTime)
// If the value is already a MongoDate instance, we don't need to parse it.
if ($value instanceof MongoDate)
{
$value = new MongoDate($value->getTimestamp());
return $value;
}
// Convert timestamp to MongoDate
elseif (is_numeric($value))
// Let Eloquent convert the value to a DateTime instance.
if ( ! $value instanceof DateTime)
{
$value = new MongoDate($value);
$value = parent::asDateTime($value);
}
return $value;
return new MongoDate($value->getTimestamp());
}
/**
......@@ -115,25 +152,23 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
*/
protected function asDateTime($value)
{
// Convert timestamp
if (is_numeric($value))
// Convert MongoDate instances.
if ($value instanceof MongoDate)
{
return Carbon::createFromTimestamp($value);
return Carbon::createFromTimestamp($value->sec);
}
// Convert string
if (is_string($value))
{
return new Carbon($value);
return parent::asDateTime($value);
}
// Convert MongoDate
if ($value instanceof MongoDate)
/**
* Get the format for database stored dates.
*
* @return string
*/
protected function getDateFormat()
{
return Carbon::createFromTimestamp($value->sec);
}
return Carbon::instance($value);
return 'Y-m-d H:i:s';
}
/**
......@@ -159,27 +194,23 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
}
/**
* Set the array of model attributes. No checking is done.
* Get an attribute from the model.
*
* @param array $attributes
* @param bool $sync
* @return void
* @param string $key
* @return mixed
*/
public function setRawAttributes(array $attributes, $sync = false)
{
foreach($attributes as $key => &$value)
public function getAttribute($key)
{
/**
* MongoIds are converted to string to make it easier to pass
* the id to other instances or relations.
*/
if ($value instanceof MongoId)
$attribute = parent::getAttribute($key);
// If the attribute is a MongoId object, return it as a string.
// This is makes Eloquent relations a lot easier.
if ($attribute instanceof MongoId)
{
$value = (string) $value;
}
return (string) $attribute;
}
parent::setRawAttributes($attributes, $sync);
return $attribute;
}
/**
......@@ -191,16 +222,20 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
{
$attributes = parent::attributesToArray();
// Because the original Eloquent never returns objects, we convert
// MongoDB related objects to a string representation. This kind
// of mimics the SQL behaviour so that dates are formatted
// nicely when your models are converted to JSON.
foreach ($attributes as &$value)
{
/**
* Here we convert MongoDate instances to string. This mimics
* original SQL behaviour so that dates are formatted nicely
* when your models are converted to JSON.
*/
if ($value instanceof MongoDate)
if ($value instanceof MongoId)
{
$value = (string) $value;
}
else if ($value instanceof MongoDate)
{
$value = $this->asDateTime($value)->format('Y-m-d H:i:s');
$value = $this->asDateTime($value)->format($this->getDateFormat());
}
}
......@@ -213,7 +248,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
* @param mixed $columns
* @return int
*/
public function dropColumn($columns)
public function drop($columns)
{
if (!is_array($columns)) $columns = array($columns);
......@@ -224,7 +259,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
}
// Perform unset only on current document
return $query = $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
return $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
}
/**
......@@ -279,7 +314,7 @@ abstract class Model extends \Jenssegers\Eloquent\Model {
// Unset method
if ($method == 'unset')
{
return call_user_func_array(array($this, 'dropColumn'), $parameters);
return call_user_func_array(array($this, 'drop'), $parameters);
}
return parent::__call($method, $parameters);
......
<?php namespace Jenssegers\Mongodb\Query;
use MongoID;
use MongoId;
use MongoRegex;
use MongoDate;
use DateTime;
......@@ -27,7 +27,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
'=', '<', '>', '<=', '>=', '<>', '!=',
'like', 'not like', 'between', 'ilike',
'&', '|', '^', '<<', '>>',
'exists', 'type', 'mod', 'where', 'all', 'size', 'regex',
'exists', 'type', 'mod', 'where', 'all', 'size', 'regex', 'elemmatch'
);
/**
......@@ -59,7 +59,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
/**
* Execute a query for a single record by ID.
*
* @param int $id
* @param mixed $id
* @param array $columns
* @return mixed
*/
......@@ -377,8 +377,8 @@ class Builder extends \Illuminate\Database\Query\Builder {
$sequence = '_id';
}
// Return id as a string
return (string) $values[$sequence];
// Return id
return $values[$sequence];
}
}
......@@ -585,7 +585,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
* @param mixed $columns
* @return int
*/
public function dropColumn($columns)
public function drop($columns)
{
if (!is_array($columns)) $columns = array($columns);
......@@ -667,23 +667,30 @@ class Builder extends \Illuminate\Database\Query\Builder {
*/
protected function compileWheres()
{
if (!$this->wheres) return array();
// The wheres to compile.
$wheres = $this->wheres ?: array();
// The new list of compiled wheres
$wheres = array();
// We will add all compiled wheres to this array.
$compiled = array();
foreach ($this->wheres as $i => &$where)
foreach ($wheres as $i => &$where)
{
// Make sure the operator is in lowercase
// Make sure the operator is in lowercase.
if (isset($where['operator']))
{
$where['operator'] = strtolower($where['operator']);
// Fix elemMatch.
if ($where['operator'] == 'elemmatch')
{
$where['operator'] = 'elemMatch';
}
}
// Convert id's
// Convert id's.
if (isset($where['column']) && $where['column'] == '_id')
{
// Multiple values
// Multiple values.
if (isset($where['values']))
{
foreach ($where['values'] as &$value)
......@@ -691,48 +698,57 @@ class Builder extends \Illuminate\Database\Query\Builder {
$value = $this->convertKey($value);
}
}
// Single value
elseif (isset($where['value']))
// Single value.
else if (isset($where['value']))
{
$where['value'] = $this->convertKey($where['value']);
}
}
// Convert dates
// Convert DateTime values to MongoDate.
if (isset($where['value']) && $where['value'] instanceof DateTime)
{
$where['value'] = new MongoDate($where['value']->getTimestamp());
}
// First item of chain
if ($i == 0 && count($this->wheres) > 1 && $where['boolean'] == 'and')
// The next item in a "chain" of wheres devices the boolean of the
// first item. So if we see that there are multiple wheres, we will
// use the operator of the next where.
if ($i == 0 and count($wheres) > 1 and $where['boolean'] == 'and')
{
// Copy over boolean value of next item in chain
$where['boolean'] = $this->wheres[$i+1]['boolean'];
$where['boolean'] = $wheres[$i+1]['boolean'];
}
// Delegate
// We use different methods to compile different wheres.
$method = "compileWhere{$where['type']}";
$compiled = $this->{$method}($where);
$result = $this->{$method}($where);
// Check for or
// Wrap the where with an $or operator.
if ($where['boolean'] == 'or')
{
$compiled = array('$or' => array($compiled));
$result = array('$or' => array($result));
}
// If there are multiple wheres, we will wrap it with $and. This is needed
// to make nested wheres work.
else if (count($wheres) > 1)
{
$result = array('$and' => array($result));
}
// Merge compiled where
$wheres = array_merge_recursive($wheres, $compiled);
// Merge the compiled where with the others.
$compiled = array_merge_recursive($compiled, $result);
}
return $wheres;
return $compiled;
}
protected function compileWhereBasic($where)
{
extract($where);
// Replace like with MongoRegex
// Replace like with a MongoRegex instance.
if ($operator == 'like')
{
$operator = '=';
......@@ -745,7 +761,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
$value = new MongoRegex("/$regex/i");
}
if (!isset($operator) || $operator == '=')
if ( ! isset($operator) or $operator == '=')
{
$query = array($column => $value);
}
......@@ -846,7 +862,7 @@ class Builder extends \Illuminate\Database\Query\Builder {
{
if ($method == 'unset')
{
return call_user_func_array(array($this, 'dropColumn'), $parameters);
return call_user_func_array(array($this, 'drop'), $parameters);
}
return parent::__call($method, $parameters);
......
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Collection;
use Jenssegers\Mongodb\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany as EloquentBelongsToMany;
class BelongsToMany extends EloquentBelongsToMany {
......@@ -36,27 +36,25 @@ class BelongsToMany extends EloquentBelongsToMany {
{
if (static::$constraints)
{
$this->query->where($this->foreignKey, $this->parent->getKey());
$this->query->where($this->getForeignKey(), '=', $this->parent->getKey());
}
}
/**
* Sync the intermediate tables with a list of IDs.
* Sync the intermediate tables with a list of IDs or collection of models.
*
* @param array $ids
* @param bool $detaching
* @return void
*/
public function sync(array $ids, $detaching = true)
public function sync($ids, $detaching = true)
{
if ($ids instanceof Collection) $ids = $ids->modelKeys();
// First we need to attach any of the associated models that are not currently
// in this joining table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->parent->{$this->otherKey};
// Check if the current array exists or not on the parent model and create it
// if it does not exist
if (is_null($current)) $current = array();
$current = $this->parent->{$this->otherKey} ?: array();
$records = $this->formatSyncList($ids);
......@@ -133,27 +131,6 @@ class BelongsToMany extends EloquentBelongsToMany {
if ($touch) $this->touchIfTouching();
}
/**
* Create an array of records to insert into the pivot table.
*
* @param array $ids
* @return void
*/
protected function createAttachRecords($ids, array $attributes)
{
$records = array();
// To create the attachment records, we will simply spin through the IDs given
// and create a new record to insert for each ID. Each ID may actually be a
// key in the array, with extra attributes to be placed in other columns.
foreach ($ids as $key => $value)
{
$records[] = $this->attacher($key, $value, $attributes, false);
}
return $records;
}
/**
* Detach models from the relationship.
*
......@@ -165,6 +142,8 @@ class BelongsToMany extends EloquentBelongsToMany {
{
if ($ids instanceof Model) $ids = (array) $ids->getKey();
$query = $this->getNewRelatedQuery();
// If associated IDs were passed to the method we will only delete those
// associations, otherwise all of the association ties will be broken.
// We'll return the numbers of affected rows when we do the deletes.
......@@ -176,9 +155,6 @@ class BelongsToMany extends EloquentBelongsToMany {
$this->parent->pull($this->otherKey, $id);
}
// Get a new related query.
$query = $this->getNewRelatedQuery();
// Prepare the query to select all related objects.
if (count($ids) > 0)
{
......@@ -238,14 +214,4 @@ class BelongsToMany extends EloquentBelongsToMany {
{
return $this->foreignKey;
}
/**
* Get the fully qualified "other key" for the relation.
*
* @return string
*/
public function getOtherKey()
{
return $this->otherKey;
}
}
<?php namespace Jenssegers\Mongodb\Relations;
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
*/
public function getResults()
{
return $this->toModel($this->getEmbedded());
}
/**
* Check if a model is already embedded.
*
* @param mixed $key
* @return bool
*/
public function contains($key)
{
if ($key instanceof Model) $key = $key->getKey();
$embedded = $this->getEmbedded();
$primaryKey = $this->related->getKeyName();
return ($embedded and $embedded[$primaryKey] == $key);
}
/**
* 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)
{
// Create a new key if needed.
if ( ! $model->getAttribute('_id'))
{
$model->setAttribute('_id', new MongoId);
}
$this->setEmbedded($model->getAttributes());
return $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);
}
$result = $this->query->update(array($this->localKey => $model->getAttributes()));
if ($result) $this->associate($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)
{
$result = $this->query->update(array($this->localKey => $model->getAttributes()));
if ($result) $this->associate($model);
return $result ? $model : false;
}
/**
* Delete all embedded models.
*
* @return int
*/
public function delete()
{
// Overwrite the local key with an empty array.
$result = $this->query->update(array($this->localKey => null));
// 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;
}
}
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Eloquent\Collection;
use MongoId;
abstract class EmbedsOneOrMany extends Relation {
/**
* The local key of the parent model.
*
* @var string
*/
protected $localKey;
/**
* The foreign key of the parent model.
*
* @var string
*/
protected $foreignKey;
/**
* The "name" of the relationship.
*
* @var string
*/
protected $relation;
/**
* Create a new embeds many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $localKey
* @param string $foreignKey
* @param string $relation
* @return void
*/
public function __construct(Builder $query, Model $parent, Model $related, $localKey, $foreignKey, $relation)
{
$this->query = $query;
$this->parent = $parent;
$this->related = $related;
$this->localKey = $localKey;
$this->foreignKey = $foreignKey;
$this->relation = $relation;
$this->addConstraints();
}
/**
* Set the base constraints on the relation query.
*
* @return void
*/
public function addConstraints()
{
if (static::$constraints)
{
$this->query->where($this->parent->getKeyName(), '=', $this->parent->getKey());
}
}
/**
* Set the constraints for an eager load of the relation.
*
* @param array $models
* @return void
*/
public function addEagerConstraints(array $models)
{
// There are no eager loading constraints.
}
/**
* Initialize the relation on a set of models.
*
* @param array $models
* @param string $relation
* @return void
*/
public function initRelation(array $models, $relation)
{
foreach ($models as $model)
{
$model->setRelation($relation, $this->related->newCollection());
}
return $models;
}
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array
*/
public function match(array $models, Collection $results, $relation)
{
foreach ($models as $model)
{
$results = $model->$relation()->getResults();
$model->setRelation($relation, $results);
}
return $models;
}
/**
* Shorthand to get the results of the relationship.
*
* @return Illuminate\Database\Eloquent\Collection
*/
public function get()
{
return $this->getResults();
}
/**
* Get the number of embedded models.
*
* @return int
*/
public function count()
{
return count($this->getEmbedded());
}
/**
* Attach a model instance to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return \Illuminate\Database\Eloquent\Model
*/
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);
if ($result)
{
$this->fireModelEvent($model, 'created', false);
// Mark model as existing
$model->exists = true;
}
}
// 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)
{
$this->fireModelEvent($result, 'saved', false);
return $result;
}
return false;
}
/**
* Create a new instance of the related model.
*
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model
*/
public function create(array $attributes)
{
// Here we will set the raw attributes to avoid hitting the "fill" method so
// that we do not have to worry about a mass accessor rules blocking sets
// on the models. Otherwise, some of these attributes will not get set.
$instance = $this->related->newInstance();
$instance->setRawAttributes($attributes);
return $this->save($instance);
}
/**
* 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'));
return $models;
}
/**
* Create an array of new instances of the related model.
*
* @param array $records
* @return array
*/
public function createMany(array $records)
{
$instances = array_map(array($this, 'create'), $records);
return $instances;
}
/**
* Transform single ID, single Model or array of Models into an array of IDs
*
* @param mixed $ids
* @return array
*/
protected function getIdsArrayFrom($ids)
{
if (! is_array($ids)) $ids = array($ids);
foreach ($ids as &$id)
{
if ($id instanceof Model) $id = $id->getKey();
}
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.
*
* @return array
*/
protected function getEmbedded()
{
// Get raw attributes to skip relations and accessors.
$attributes = $this->parent->getAttributes();
return isset($attributes[$this->localKey]) ? $attributes[$this->localKey] : null;
}
/**
* Set the embedded records array.
*
* @param array $models
* @return void
*/
protected function setEmbedded($data)
{
$attributes = $this->parent->getAttributes();
$attributes[$this->localKey] = $data;
// 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);
}
}
/**
* Get the foreign key value for the relation.
*
* @param mixed $id
* @return mixed
*/
protected function getForeignKeyValue($id)
{
if ($id instanceof Model)
{
$id = $id->getKey();
}
// Convert the id to MongoId if necessary.
return $this->getBaseQuery()->convertKey($id);
}
/**
* Convert an array of records to a Collection.
*
* @param array $records
* @return Illuminate\Database\Eloquent\Collection
*/
protected function toCollection(array $records = array())
{
$models = array();
// Wrap records in model objects.
foreach ($records as $attributes)
{
$models[] = $this->toModel($attributes);
}
if (count($models) > 0)
{
$models = $this->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
}
/**
* Fire the given event for the given model.
*
* @param string $event
* @param bool $halt
* @return mixed
*/
protected function fireModelEvent(Model $model, $event, $halt = true)
{
$dispatcher = $model->getEventDispatcher();
if ( is_null($dispatcher)) return true;
// 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);
$method = $halt ? 'until' : 'fire';
return $dispatcher->$method($event, $model);
}
}
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany as EloquentHasMany;
class HasMany extends EloquentHasMany {
/**
* Add the constraints for a relationship count query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parent
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationCountQuery(Builder $query, Builder $parent)
{
$foreignKey = $this->getHasCompareKey();
return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true);
}
}
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasOne as EloquentHasOne;
class HasOne extends EloquentHasOne {
/**
* Add the constraints for a relationship count query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Builder $parent
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getRelationCountQuery(Builder $query, Builder $parent)
{
$foreignKey = $this->getHasCompareKey();
return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true);
}
}
<?php namespace Jenssegers\Mongodb\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Database\Eloquent\Relations\MorphTo as EloquentMorphTo;
class MorphTo extends BelongsTo {
class MorphTo extends EloquentMorphTo {
/**
* The type of the polymorphic relation.
* Set the base constraints on the relation query.
*
* @var string
*/
protected $morphType;
/**
* The models whose relations are being eager loaded.
*
* @var \Illuminate\Database\Eloquent\Collection
*/
protected $models;
/**
* All of the models keyed by ID.
*
* @var array
*/
protected $dictionary = array();
/**
* Create a new belongs to relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \Illuminate\Database\Eloquent\Model $parent
* @param string $foreignKey
* @param string $otherKey
* @param string $type
* @param string $relation
* @return void
*/
public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relation)
public function addConstraints()
{
$this->morphType = $type;
parent::__construct($query, $parent, $foreignKey, $otherKey, $relation);
if (static::$constraints)
{
// For belongs to relationships, which are essentially the inverse of has one
// or has many relationships, we need to actually query on the primary key
// of the related models matching on the foreign key that's on a parent.
$this->query->where($this->otherKey, '=', $this->parent->{$this->foreignKey});
}
}
/**
......@@ -58,125 +32,4 @@ class MorphTo extends BelongsTo {
$this->buildDictionary($this->models = Collection::make($models));
}
/**
* Buiild a dictionary with the models.
*
* @param \Illuminate\Database\Eloquent\Models $models
* @return void
*/
protected function buildDictionary(Collection $models)
{
foreach ($models as $model)
{
if ($model->{$this->morphType})
{
$this->dictionary[$model->{$this->morphType}][$model->{$this->foreignKey}][] = $model;
}
}
}
/**
* Match the eagerly loaded results to their parents.
*
* @param array $models
* @param \Illuminate\Database\Eloquent\Collection $results
* @param string $relation
* @return array
*/
public function match(array $models, Collection $results, $relation)
{
return $models;
}
/**
* Get the results of the relationship.
*
* Called via eager load method of Eloquent query builder.
*
* @return mixed
*/
public function getEager()
{
foreach (array_keys($this->dictionary) as $type)
{
$this->matchToMorphParents($type, $this->getResultsByType($type));
}
return $this->models;
}
/**
* Match the results for a given type to their parents.
*
* @param string $type
* @param \Illuminate\Database\Eloquent\Collection $results
* @return void
*/
protected function matchToMorphParents($type, Collection $results)
{
foreach ($results as $result)
{
if (isset($this->dictionary[$type][$result->getKey()]))
{
foreach ($this->dictionary[$type][$result->getKey()] as $model)
{
$model->setRelation($this->relation, $result);
}
}
}
}
/**
* Get all of the relation results for a type.
*
* @param string $type
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getResultsByType($type)
{
$instance = $this->createModelByType($type);
$key = $instance->getKeyName();
return $instance->whereIn($key, $this->gatherKeysByType($type)->all())->get();
}
/**
* Gather all of the foreign keys for a given type.
*
* @param string $type
* @return array
*/
protected function gatherKeysByType($type)
{
$foreign = $this->foreignKey;
return BaseCollection::make($this->dictionary[$type])->map(function($models) use ($foreign)
{
return head($models)->{$foreign};
})->unique();
}
/**
* Create a new model instance by type.
*
* @param string $type
* @return \Illuminate\Database\Eloquent\Model
*/
public function createModelByType($type)
{
return new $type;
}
/**
* Get the dictionary used by the relationship.
*
* @return array
*/
public function getDictionary()
{
return $this->dictionary;
}
}
......@@ -6,7 +6,10 @@ class ConnectionTest extends TestCase {
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf('Jenssegers\Mongodb\Connection', $connection);
}
public function testReconnect()
{
$c1 = DB::connection('mongodb');
$c2 = DB::connection('mongodb');
$this->assertEquals(spl_object_hash($c1), spl_object_hash($c2));
......@@ -20,6 +23,9 @@ class ConnectionTest extends TestCase {
{
$connection = DB::connection('mongodb');
$this->assertInstanceOf('MongoDB', $connection->getMongoDB());
$connection = DB::connection('mongodb');
$this->assertInstanceOf('MongoClient', $connection->getMongoClient());
}
public function testCollection()
......@@ -81,4 +87,33 @@ class ConnectionTest extends TestCase {
$this->assertInstanceOf('Jenssegers\Mongodb\Schema\Builder', $schema);
}
public function testDriverName()
{
$driver = DB::connection('mongodb')->getDriverName();
$this->assertEquals('mongodb', $driver);
}
public function testAuth()
{
Config::set('database.connections.mongodb.username', 'foo');
Config::set('database.connections.mongodb.password', 'bar');
$host = Config::get('database.connections.mongodb.host');
$port = Config::get('database.connections.mongodb.port', 27017);
$database = Config::get('database.connections.mongodb.database');
$this->setExpectedException('MongoConnectionException', "Failed to connect to: $host:$port: Authentication failed on database '$database' with username 'foo': auth fails");
$connection = DB::connection('mongodb');
}
public function testCustomPort()
{
$port = 27000;
Config::set('database.connections.mongodb.port', $port);
$host = Config::get('database.connections.mongodb.host');
$database = Config::get('database.connections.mongodb.database');
$this->setExpectedException('MongoConnectionException', "Failed to connect to: $host:$port: Connection refused");
$connection = DB::connection('mongodb');
}
}
This diff is collapsed.
......@@ -34,10 +34,14 @@ class ModelTest extends TestCase {
$this->assertEquals(1, User::count());
$this->assertTrue(isset($user->_id));
$this->assertTrue(is_string($user->_id));
$this->assertNotEquals('', (string) $user->_id);
$this->assertNotEquals(0, strlen((string) $user->_id));
$this->assertInstanceOf('Carbon\Carbon', $user->created_at);
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$this->assertEquals('John Doe', $user->name);
$this->assertEquals(35, $user->age);
}
......@@ -50,6 +54,9 @@ class ModelTest extends TestCase {
$user->age = 35;
$user->save();
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$check = User::find($user->_id);
$check->age = 36;
......@@ -65,6 +72,9 @@ class ModelTest extends TestCase {
$user->update(array('age' => 20));
$raw = $user->getAttributes();
$this->assertInstanceOf('MongoId', $raw['_id']);
$check = User::find($user->_id);
$this->assertEquals(20, $check->age);
}
......@@ -277,6 +287,7 @@ class ModelTest extends TestCase {
$this->assertEquals(array('_id', 'created_at', 'name', 'type', 'updated_at'), $keys);
$this->assertTrue(is_string($array['created_at']));
$this->assertTrue(is_string($array['updated_at']));
$this->assertTrue(is_string($array['_id']));
}
public function testUnset()
......@@ -308,7 +319,8 @@ class ModelTest extends TestCase {
public function testDates()
{
$user = User::create(array('name' => 'John Doe', 'birthday' => new DateTime('1980/1/1')));
$birthday = new DateTime('1980/1/1');
$user = User::create(array('name' => 'John Doe', 'birthday' => $birthday));
$this->assertInstanceOf('Carbon\Carbon', $user->birthday);
$check = User::find($user->_id);
......@@ -317,6 +329,25 @@ class ModelTest extends TestCase {
$user = User::where('birthday', '>', new DateTime('1975/1/1'))->first();
$this->assertEquals('John Doe', $user->name);
// test custom date format for json output
$json = $user->toArray();
$this->assertEquals($user->birthday->format('l jS \of F Y h:i:s A'), $json['birthday']);
$this->assertEquals($user->created_at->format('l jS \of F Y h:i:s A'), $json['created_at']);
// test default date format for json output
$item = Item::create(array('name' => 'sword'));
$json = $item->toArray();
$this->assertEquals($item->created_at->format('Y-m-d H:i:s'), $json['created_at']);
$user = User::create(array('name' => 'Jane Doe', 'birthday' => time()));
$this->assertInstanceOf('Carbon\Carbon', $user->birthday);
$user = User::create(array('name' => 'Jane Doe', 'birthday' => 'Monday 8th of August 2005 03:12:46 PM'));
$this->assertInstanceOf('Carbon\Carbon', $user->birthday);
$user = User::create(array('name' => 'Jane Doe', 'birthday' => '2005-08-08'));
$this->assertInstanceOf('Carbon\Carbon', $user->birthday);
}
public function testIdAttribute()
......
......@@ -45,7 +45,7 @@ class MysqlRelationsTest extends TestCase {
$user = MysqlUser::find($user->id); // refetch
$this->assertEquals('admin', $user->role->type);
// MongoDB beelongs to
// MongoDB belongs to
$role = $user->role()->first(); // refetch
$this->assertEquals('John Doe', $role->mysqlUser->name);
......
......@@ -54,9 +54,7 @@ class QueryBuilderTest extends TestCase {
public function testInsertGetId()
{
$id = DB::collection('users')->insertGetId(array('name' => 'John Doe'));
$this->assertTrue(is_string($id));
$this->assertEquals(24, strlen($id));
$this->assertInstanceOf('MongoId', $id);
}
public function testBatchInsert()
......@@ -534,6 +532,27 @@ class QueryBuilderTest extends TestCase {
$results = DB::collection('users')->where('name', 'REGEX', $regex)->get();
$this->assertEquals(2, count($results));
DB::collection('users')->insert(array(
array(
'name' => 'John Doe',
'addresses' => array(
array('city' => 'Ghent'),
array('city' => 'Paris')
)
),
array(
'name' => 'Jane Doe',
'addresses' => array(
array('city' => 'Brussels'),
array('city' => 'Paris')
)
)
));
$users = DB::collection('users')->where('addresses', 'elemMatch', array('city' => 'Brussels'))->get();
$this->assertEquals(1, count($users));
$this->assertEquals('Jane Doe', $users[0]['name']);
}
public function testIncrement()
......
......@@ -255,4 +255,29 @@ class QueryTest extends TestCase {
$this->assertEquals(6, count($users));
}
public function testMultipleOr()
{
$users = User::where(function($query)
{
$query->where('age', 35)->orWhere('age', 33);
})
->where(function($query)
{
$query->where('name', 'John Doe')->orWhere('name', 'Jane Doe');
})->get();
$this->assertEquals(2, count($users));
$users = User::where(function($query)
{
$query->orWhere('age', 35)->orWhere('age', 33);
})
->where(function($query)
{
$query->orWhere('name', 'John Doe')->orWhere('name', 'Jane Doe');
})->get();
$this->assertEquals(2, count($users));
}
}
This diff is collapsed.
......@@ -114,11 +114,11 @@ class SchemaTest extends TestCase {
{
Schema::collection('newcollection', function($collection)
{
$collection->background('backgroundkey');
$collection->sparse('sparsekey');
});
$index = $this->getIndex('newcollection', 'backgroundkey');
$this->assertEquals(1, $index['background']);
$index = $this->getIndex('newcollection', 'sparsekey');
$this->assertEquals(1, $index['sparse']);
}
public function testExpire()
......
......@@ -32,6 +32,9 @@ class TestCase extends Orchestra\Testbench\TestCase {
// overwrite database configuration
$app['config']->set('database.connections.mysql', $config['connections']['mysql']);
$app['config']->set('database.connections.mongodb', $config['connections']['mongodb']);
// overwrite cache configuration
$app['config']->set('cache.driver', 'array');
}
}
......@@ -16,7 +16,7 @@ class MysqlRole extends Eloquent {
public function mysqlUser()
{
return $this->belongsTo('MysqlUser', 'role_id');
return $this->belongsTo('MysqlUser');
}
/**
......
......@@ -16,7 +16,7 @@ class MysqlUser extends Eloquent {
public function role()
{
return $this->hasOne('Role', 'role_id');
return $this->hasOne('Role');
}
/**
......
......@@ -14,7 +14,7 @@ class Role extends Eloquent {
public function mysqlUser()
{
return $this->belongsTo('MysqlUser', 'role_id');
return $this->belongsTo('MysqlUser');
}
}
......@@ -7,7 +7,6 @@ use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
protected $collection = 'users';
protected $dates = array('birthday');
protected static $unguarded = true;
......@@ -56,6 +55,11 @@ class User extends Eloquent implements UserInterface, RemindableInterface {
return $this->embedsMany('Address');
}
public function father()
{
return $this->embedsOne('User');
}
/**
* Get the unique identifier for the user.
*
......@@ -85,4 +89,39 @@ class User extends Eloquent implements UserInterface, RemindableInterface {
{
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