Skip to content

Added support for ObjectIds in fields other than _id & improved date casting #1523

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

ARG PHP_VERSION=7.2

FROM php:${PHP_VERSION}-cli
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,25 @@ $users = User::where('birthday', '>', new DateTime('-18 years'))->get();

### Relations

By default, relationships are not stored as ObjectIds. If you need relationships to be stored as ObjectIds, you can specify that they should be by adding them to the $casts array:

```php
use Jenssegers\Mongodb\Eloquent\Model as Eloquent;

class User extends Eloquent {

protected $casts = [
'account_id' => 'objectid' // 'objectid' has to be lowercase!
];

public function account()
{
return $this->belongsTo('Account');
}

}
```

Supported relations are:

- hasOne
Expand Down
289 changes: 265 additions & 24 deletions src/Jenssegers/Mongodb/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ protected function asDateTime($value)
return Carbon::createFromTimestamp($value->toDateTime()->getTimestamp());
}

if ($value instanceof Carbon) {
return $value;
}

return parent::asDateTime($value);
}

Expand Down Expand Up @@ -159,53 +163,144 @@ protected function getAttributeFromArray($key)
return parent::getAttributeFromArray($key);
}

/**
* Is the given array an associative array
*
* @param array $arr
*
* @return bool
*/
private function isAssoc(array $arr)
{
// if it's empty, we presume that it's sequential
if ([] === $arr) {
return false;
}

// if $arr[0] does not exist, it must to be a associative array
if (!array_key_exists(0, $arr)) {
return true;
}

// strict compare the array keys against a range of the same length
return array_keys($arr) !== range(0, count($arr) - 1);
}

/**
* Flatten an associative array of attributes to dot notation
*
* @param string $key
* @param $value
*
* @return \Illuminate\Support\Collection
*/
private function flattenAttributes($key, $value)
{
$values = collect();

if (is_array($value) && $this->isAssoc($value)) {
foreach ($value as $k => $value) {
$values = $values->merge($this->flattenAttributes("$key.$k", $value));
}
} else {
$values->put($key, $value);
}

return $values;
}

/**
* @inheritdoc
*/
public function setAttribute($key, $value)
{
// Convert _id to ObjectID.
if ($key == '_id' && 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);
$values = $this->flattenAttributes($key, $value);

$values->each(function ($val, $k) {
// Convert to ObjectID.
if ($this->isCastableToObjectId($k)) {
$builder = $this->newBaseQueryBuilder();

if (is_array($val)) {
foreach ($val as &$v) {
$v = $builder->convertKey($v);
}
} else {
$val = $builder->convertKey($val);
}
} // Convert to UTCDateTime
else {
if (in_array($k, $this->getDates()) && $val) {
if (is_array($val)) {
foreach ($val as &$v) {
$v = $this->fromDateTime($v);
}
} else {
$val = $this->fromDateTime($val);
}
}
}

Arr::set($this->attributes, $key, $value);
// Support keys in dot notation.
if (Str::contains($k, '.')) {
Arr::set($this->attributes, $k, $val);

return;
}
return;
}

parent::setAttribute($k, $val);
});

return parent::setAttribute($key, $value);
return $this;
}

/**
* @inheritdoc
*/
public function attributesToArray()
{
// Convert dates before parent method call so that all dates have the same format
foreach ($this->getDates() as $key) {
if (!Arr::has($this->attributes, $key)) {
continue;
}

$val = Arr::get($this->attributes, $key);

if (is_array($val)) {
foreach ($val as &$v) {
$v = $this->asDateTime($v)->format('Y-m-d H:i:s.v');
}
} elseif ($val) {
$val = $this->asDateTime($val)->format('Y-m-d H:i:s.v');
}

Arr::set($this->attributes, $key, $val);
}

$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 $key => &$value) {
if ($value instanceof ObjectID) {
$value = (string) $value;
$this->getObjectIds()->each(function ($key) use (&$attributes) {
if (!Arr::has($attributes, $key)) {
return;
}
}

// Convert dot-notation dates.
foreach ($this->getDates() as $key) {
if (Str::contains($key, '.') && Arr::has($attributes, $key)) {
Arr::set($attributes, $key, (string) $this->asDateTime(Arr::get($attributes, $key)));
$value = Arr::get($attributes, $key);

if (is_array($value)) {
foreach ($value as &$val) {
$val = (string) $val;
}
} else {
$value = (string) $value;
}
}

Arr::set($attributes, $key, $value);
});

return $attributes;
}
Expand All @@ -218,6 +313,18 @@ public function getCasts()
return $this->casts;
}

/**
* Get a list of the castable ObjectId fields
*
* @return \Illuminate\Support\Collection
*/
public function getObjectIds()
{
return collect($this->getCasts())->filter(function ($val) {
return 'objectid' === $val;
})->keys()->push('_id');
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -293,6 +400,22 @@ public function push()

$this->pushAttributeValues($column, $values, $unique);

// Convert $casts on push()
if ($this->isCastableToObjectId($column)) {
foreach ($values as &$value) {
if (is_string($value)) {
$value = new ObjectId($value);
}
}
}

// Convert dates on push()
if (in_array($column, $this->getDates())) {
foreach ($values as &$value) {
$value = $this->fromDateTime($value);
}
}

return $query->push($column, $values, $unique);
}

Expand Down Expand Up @@ -338,11 +461,94 @@ protected function pushAttributeValues($column, array $values, $unique = false)
$current[] = $value;
}

$this->attributes[$column] = $current;
// Support for dot notation keys
if (Str::contains($column, '.')) {
Arr::set($this->attributes, $column, $current);
} else {
$this->attributes[$column] = $current;
}

$this->syncOriginalAttribute($column);
}

/**
* @inheritdoc
*/
public function syncOriginalAttribute($attribute)
{
// Support for dot notation keys
if (Str::contains($attribute, '.')) {
$value = Arr::get($this->attributes, $attribute);

Arr::set($this->original, $attribute, $value);
} else {
$this->original[$attribute] = $this->attributes[$attribute];
}

return $this;
}

/**
* @inheritdoc
*/
protected function addDateAttributesToArray(array $attributes)
{
foreach ($this->getDates() as $key) {
if (!isset($attributes[$key])) {
continue;
}

// Support for array of dates
if (is_array($attributes[$key])) {
foreach ($attributes[$key] as &$value) {
$value = $this->serializeDate($this->asDateTime($value));
}
} else {
$attributes[$key] = $this->serializeDate($this->asDateTime($attributes[$key]));
}
}

return $attributes;
}

/**
* @inheritdoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add dock block for $key

*/
public function getAttributeValue($key)
{
$value = $this->getAttributeFromArray($key);

// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if (in_array($key, $this->getDates()) && !is_null($value)) {
if (is_array($value)) {
foreach ($value as &$val) {
$val = $this->asDateTime($val);
}
} else {
$value = $this->asDateTime($value);
}

return $value;
}

// Also convert objectIds to strings
if ($this->getObjectIds()->search($key) !== false && !is_null($value)) {
if (is_array($value)) {
foreach ($value as &$val) {
$val = (string) $val;
}
} else {
$value = (string) $value;
}

return $value;
}

return parent::getAttributeFromArray($key);
}

/**
* Remove one or more values to the underlying attribute value and sync with original.
*
Expand Down Expand Up @@ -411,7 +617,7 @@ protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();

return new QueryBuilder($connection, $connection->getPostProcessor());
return (new QueryBuilder($connection, $connection->getPostProcessor()))->casts($this->casts);
}

/**
Expand Down Expand Up @@ -480,4 +686,39 @@ public function __call($method, $parameters)

return parent::__call($method, $parameters);
}

/**
* @inheritdoc
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fill params please in dock block

*/
protected function castAttribute($key, $value)
{
if (is_null($value)) {
return $value;
}

if ($this->getCastType($key) === 'objectid') {
if (is_array($value)) {
foreach ($value as &$val) {
$val = (string) $val;
}

return $value;
}

return (string) $value;
}

return parent::castAttribute($key, $value);
}

/**
* Is the field an ObjectId
*
* @param string $key
* @return bool
*/
protected function isCastableToObjectId($key)
{
return '_id' === $key || $this->hasCast($key, ['objectid']);
}
}
Loading