Model.php 15.4 KB
Newer Older
1
<?php namespace Jenssegers\Mongodb\Eloquent;
Jens Segers's avatar
Jens Segers committed
2

3
use Carbon\Carbon;
Jens Segers's avatar
Jens Segers committed
4
use DateTime;
5
use Illuminate\Database\Eloquent\Model as BaseModel;
6
use Illuminate\Database\Eloquent\Relations\Relation;
7
use Jenssegers\Mongodb\Query\Builder as QueryBuilder;
8
use Jenssegers\Mongodb\Relations\EmbedsMany;
9
use Jenssegers\Mongodb\Relations\EmbedsOne;
10
use Jenssegers\Mongodb\Relations\EmbedsOneOrMany;
Jens Segers's avatar
Jens Segers committed
11 12
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\UTCDateTime;
13
use ReflectionMethod;
Jens Segers's avatar
Jens Segers committed
14

Jens Segers's avatar
Jens Segers committed
15 16
abstract class Model extends BaseModel
{
17
    use HybridRelations;
Jens Segers's avatar
Jens Segers committed
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

    /**
     * The collection associated with the model.
     *
     * @var string
     */
    protected $collection;

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = '_id';

33 34 35 36 37
    /**
     * The parent relation instance.
     *
     * @var Relation
     */
38
    protected $parentRelation;
39

Jens Segers's avatar
Jens Segers committed
40 41 42
    /**
     * Custom accessor for the model's id.
     *
43
     * @param  mixed  $value
Kévin Bargoin's avatar
Kévin Bargoin committed
44
     * @return mixed
Jens Segers's avatar
Jens Segers committed
45 46 47
     */
    public function getIdAttribute($value)
    {
Jens Segers's avatar
Jens Segers committed
48 49
        // If we don't have a value for 'id', we will use the Mongo '_id' value.
        // This allows us to work with models in a more sql-like way.
Jens Segers's avatar
Jens Segers committed
50
        if (! $value and array_key_exists('_id', $this->attributes)) {
51
            $value = $this->attributes['_id'];
52
        }
Jens Segers's avatar
Jens Segers committed
53

Jens Segers's avatar
Jens Segers committed
54
        // Convert ObjectID to string.
Jens Segers's avatar
Jens Segers committed
55
        if ($value instanceof ObjectID) {
56
            return (string) $value;
57
        }
58 59

        return $value;
Jens Segers's avatar
Jens Segers committed
60
    }
61

62 63 64 65 66 67 68
    /**
     * Get the table qualified key name.
     *
     * @return string
     */
    public function getQualifiedKeyName()
    {
Jens Segers's avatar
Jens Segers committed
69
        return $this->getKeyName();
70
    }
71

72 73 74 75
    /**
     * Define an embedded one-to-many relationship.
     *
     * @param  string  $related
76 77 78
     * @param  string  $localKey
     * @param  string  $foreignKey
     * @param  string  $relation
79
     * @return \Jenssegers\Mongodb\Relations\EmbedsMany
80 81 82 83 84 85
     */
    protected function embedsMany($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.
Jens Segers's avatar
Jens Segers committed
86
        if (is_null($relation)) {
87 88 89 90 91
            list(, $caller) = debug_backtrace(false);

            $relation = $caller['function'];
        }

Jens Segers's avatar
Jens Segers committed
92
        if (is_null($localKey)) {
93
            $localKey = $relation;
94 95
        }

Jens Segers's avatar
Jens Segers committed
96
        if (is_null($foreignKey)) {
97 98 99
            $foreignKey = snake_case(class_basename($this));
        }

100 101
        $query = $this->newQuery();

102 103 104
        $instance = new $related;

        return new EmbedsMany($query, $this, $instance, $localKey, $foreignKey, $relation);
105 106
    }

107 108 109 110
    /**
     * Define an embedded one-to-many relationship.
     *
     * @param  string  $related
111 112 113 114
     * @param  string  $localKey
     * @param  string  $foreignKey
     * @param  string  $relation
     * @return \Jenssegers\Mongodb\Relations\EmbedsOne
115 116 117 118 119 120
     */
    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.
Jens Segers's avatar
Jens Segers committed
121
        if (is_null($relation)) {
122 123 124 125 126
            list(, $caller) = debug_backtrace(false);

            $relation = $caller['function'];
        }

Jens Segers's avatar
Jens Segers committed
127
        if (is_null($localKey)) {
128
            $localKey = $relation;
129 130
        }

Jens Segers's avatar
Jens Segers committed
131
        if (is_null($foreignKey)) {
132 133 134 135 136 137 138 139 140 141
            $foreignKey = snake_case(class_basename($this));
        }

        $query = $this->newQuery();

        $instance = new $related;

        return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation);
    }

Jens Segers's avatar
Jens Segers committed
142
    /**
Jens Segers's avatar
Jens Segers committed
143
     * Convert a DateTime to a storable UTCDateTime object.
Jens Segers's avatar
Jens Segers committed
144
     *
145
     * @param  DateTime|int  $value
Jens Segers's avatar
Jens Segers committed
146
     * @return UTCDateTime
Jens Segers's avatar
Jens Segers committed
147
     */
leekaiwei's avatar
leekaiwei committed
148
    public function fromDateTime($value)
Jens Segers's avatar
Jens Segers committed
149
    {
Jens Segers's avatar
Jens Segers committed
150
        // If the value is already a UTCDateTime instance, we don't need to parse it.
Jens Segers's avatar
Jens Segers committed
151
        if ($value instanceof UTCDateTime) {
Jens Segers's avatar
Jens Segers committed
152
            return $value;
153 154
        }

Jens Segers's avatar
Jens Segers committed
155
        // Let Eloquent convert the value to a DateTime instance.
Jens Segers's avatar
Jens Segers committed
156
        if (! $value instanceof DateTime) {
Jens Segers's avatar
Jens Segers committed
157
            $value = parent::asDateTime($value);
158 159
        }

Jens Segers's avatar
Jens Segers committed
160
        return new UTCDateTime($value->getTimestamp() * 1000);
Jens Segers's avatar
Jens Segers committed
161 162 163
    }

    /**
Jens Segers's avatar
Jens Segers committed
164
     * Return a timestamp as DateTime object.
Jens Segers's avatar
Jens Segers committed
165
     *
Jens Segers's avatar
Jens Segers committed
166 167
     * @param  mixed  $value
     * @return DateTime
Jens Segers's avatar
Jens Segers committed
168
     */
Jens Segers's avatar
Jens Segers committed
169
    protected function asDateTime($value)
Jens Segers's avatar
Jens Segers committed
170
    {
Jens Segers's avatar
Jens Segers committed
171
        // Convert UTCDateTime instances.
Jens Segers's avatar
Jens Segers committed
172
        if ($value instanceof UTCDateTime) {
Jens Segers's avatar
Jens Segers committed
173
            return Carbon::createFromTimestamp($value->toDateTime()->getTimestamp());
Jens Segers's avatar
Jens Segers committed
174 175
        }

Jens Segers's avatar
Jens Segers committed
176
        return parent::asDateTime($value);
Jens Segers's avatar
Jens Segers committed
177 178
    }

179 180 181 182 183 184 185
    /**
     * Get the format for database stored dates.
     *
     * @return string
     */
    protected function getDateFormat()
    {
186
        return $this->dateFormat ?: 'Y-m-d H:i:s';
187 188
    }

Jens Segers's avatar
Jens Segers committed
189
    /**
190 191
     * Get a fresh timestamp for the model.
     *
Jens Segers's avatar
Jens Segers committed
192
     * @return UTCDateTime
193
     */
Jens Segers's avatar
Jens Segers committed
194
    public function freshTimestamp()
Jens Segers's avatar
Jens Segers committed
195 196
    {
        return new UTCDateTime(round(microtime(true) * 1000));
Jens Segers's avatar
Jens Segers committed
197 198
    }

Jens Segers's avatar
Jens Segers committed
199 200 201 202 203 204 205
    /**
     * Get the table associated with the model.
     *
     * @return string
     */
    public function getTable()
    {
206
        return $this->collection ?: parent::getTable();
Jens Segers's avatar
Jens Segers committed
207 208
    }

209 210 211 212 213 214 215 216 217
    /**
     * Get an attribute from the model.
     *
     * @param  string  $key
     * @return mixed
     */
    public function getAttribute($key)
    {
        // Check if the key is an array dot notation.
Jens Segers's avatar
Jens Segers committed
218
        if (str_contains($key, '.') and array_has($this->attributes, $key)) {
Jens Segers's avatar
Jens Segers committed
219
            return $this->getAttributeValue($key);
220 221
        }

222 223 224 225 226
        $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.
Jens Segers's avatar
Jens Segers committed
227
        if (method_exists($this, $camelKey)) {
Jens Segers's avatar
Jens Segers committed
228
            $method = new ReflectionMethod(get_called_class(), $camelKey);
229

Jens Segers's avatar
Jens Segers committed
230
            // Ensure the method is not static to avoid conflicting with Eloquent methods.
Jens Segers's avatar
Jens Segers committed
231
            if (! $method->isStatic()) {
Jens Segers's avatar
Jens Segers committed
232 233 234 235
                $relations = $this->$camelKey();

                // This attribute matches an embedsOne or embedsMany relation so we need
                // to return the relation results instead of the interal attributes.
Pooya Parsa's avatar
Pooya Parsa committed
236 237 238 239 240 241 242 243 244 245 246 247 248
                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);
                }

                if ($relations instanceof Relation) {
Jens Segers's avatar
Jens Segers committed
249 250 251
                    // 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.
Pooya Parsa's avatar
Pooya Parsa committed
252
                    if (array_key_exists($key, $this->relations) && $this->relations[$key] != null) {
Jens Segers's avatar
Jens Segers committed
253 254 255 256 257
                        return $this->relations[$key];
                    }

                    // Get the relation results.
                    return $this->getRelationshipFromMethod($key, $camelKey);
258 259 260 261
                }
            }
        }

262 263 264 265 266 267 268 269 270 271 272
        return parent::getAttribute($key);
    }

    /**
     * Get an attribute from the $attributes array.
     *
     * @param  string  $key
     * @return mixed
     */
    protected function getAttributeFromArray($key)
    {
273
        // Support keys in dot notation.
Jens Segers's avatar
Jens Segers committed
274
        if (str_contains($key, '.')) {
275 276
            $attributes = array_dot($this->attributes);

Jens Segers's avatar
Jens Segers committed
277
            if (array_key_exists($key, $attributes)) {
278 279 280
                return $attributes[$key];
            }
        }
281 282

        return parent::getAttributeFromArray($key);
283 284
    }

285
    /**
286
     * Set a given attribute on the model.
287
     *
288
     * @param  string  $key
289
     * @param  mixed   $value
290
     */
291
    public function setAttribute($key, $value)
292
    {
Jens Segers's avatar
Jens Segers committed
293
        // Convert _id to ObjectID.
Jens Segers's avatar
Jens Segers committed
294
        if ($key == '_id' and is_string($value)) {
Jens Segers's avatar
Jens Segers committed
295
            $builder = $this->newBaseQueryBuilder();
296

Jens Segers's avatar
Jens Segers committed
297
            $value = $builder->convertKey($value);
Jens Segers's avatar
Jens Segers committed
298
        }
Jens Segers's avatar
Jens Segers committed
299

Jens Segers's avatar
Jens Segers committed
300
        // Support keys in dot notation.
Jens Segers's avatar
Jens Segers committed
301 302
        elseif (str_contains($key, '.')) {
            if (in_array($key, $this->getDates()) && $value) {
Jens Segers's avatar
Jens Segers committed
303 304 305
                $value = $this->fromDateTime($value);
            }

Jens Segers's avatar
Jens Segers committed
306 307
            array_set($this->attributes, $key, $value);

308
            return;
Jens Segers's avatar
Jens Segers committed
309 310
        }

Jens Segers's avatar
Jens Segers committed
311
        parent::setAttribute($key, $value);
Jens Segers's avatar
Jens Segers committed
312
    }
313

Jens Segers's avatar
Jens Segers committed
314 315 316 317 318 319 320 321 322
    /**
     * Convert the model's attributes to an array.
     *
     * @return array
     */
    public function attributesToArray()
    {
        $attributes = parent::attributesToArray();

323 324 325 326
        // 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.
Jens Segers's avatar
Jens Segers committed
327 328
        foreach ($attributes as $key => &$value) {
            if ($value instanceof ObjectID) {
329 330
                $value = (string) $value;
            }
331 332
        }

Jens Segers's avatar
Jens Segers committed
333
        // Convert dot-notation dates.
Jens Segers's avatar
Jens Segers committed
334 335
        foreach ($this->getDates() as $key) {
            if (str_contains($key, '.') and array_has($attributes, $key)) {
Jens Segers's avatar
Jens Segers committed
336 337 338 339
                array_set($attributes, $key, (string) $this->asDateTime(array_get($attributes, $key)));
            }
        }

Jens Segers's avatar
Jens Segers committed
340
        return $attributes;
341 342
    }

Jens Segers's avatar
Jens Segers committed
343 344 345 346 347
    /**
     * Get the casts array.
     *
     * @return array
     */
348
    public function getCasts()
Jens Segers's avatar
Jens Segers committed
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
    {
        return $this->casts;
    }

    /**
     * Determine if the new and old values for a given key are numerically equivalent.
     *
     * @param  string  $key
     * @return bool
     */
    protected function originalIsNumericallyEquivalent($key)
    {
        $current = $this->attributes[$key];
        $original = $this->original[$key];

        // Date comparison.
Jens Segers's avatar
Jens Segers committed
365
        if (in_array($key, $this->getDates())) {
Jens Segers's avatar
Jens Segers committed
366 367 368 369 370 371 372 373 374
            $current = $current instanceof UTCDateTime ? $this->asDateTime($current) : $current;
            $original = $original instanceof UTCDateTime ? $this->asDateTime($original) : $original;

            return $current == $original;
        }

        return parent::originalIsNumericallyEquivalent($key);
    }

Jens Segers's avatar
Jens Segers committed
375 376 377
    /**
     * Remove one or more fields.
     *
378
     * @param  mixed  $columns
Jens Segers's avatar
Jens Segers committed
379 380
     * @return int
     */
381
    public function drop($columns)
Jens Segers's avatar
Jens Segers committed
382
    {
Jens Segers's avatar
Jens Segers committed
383 384 385
        if (! is_array($columns)) {
            $columns = [$columns];
        }
Jens Segers's avatar
Jens Segers committed
386 387

        // Unset attributes
Jens Segers's avatar
Jens Segers committed
388
        foreach ($columns as $column) {
Jens Segers's avatar
Jens Segers committed
389 390
            $this->__unset($column);
        }
unknown's avatar
unknown committed
391

Jens Segers's avatar
Jens Segers committed
392
        // Perform unset only on current document
393
        return $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
Jens Segers's avatar
Jens Segers committed
394 395
    }

396
    /**
397
     * Append one or more values to an array.
398 399 400 401 402
     *
     * @return mixed
     */
    public function push()
    {
Jens Segers's avatar
Jens Segers committed
403
        if ($parameters = func_get_args()) {
404 405
            $unique = false;

Jens Segers's avatar
Jens Segers committed
406
            if (count($parameters) == 3) {
407
                list($column, $values, $unique) = $parameters;
Jens Segers's avatar
Jens Segers committed
408
            } else {
409 410 411
                list($column, $values) = $parameters;
            }

412
            // Do batch push by default.
Jens Segers's avatar
Jens Segers committed
413 414 415
            if (! is_array($values)) {
                $values = [$values];
            }
416

Jens Segers's avatar
Jens Segers committed
417
            $query = $this->setKeysForSaveQuery($this->newQuery());
418

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

            return $query->push($column, $values, $unique);
422 423 424 425 426
        }

        return parent::push();
    }

427 428 429
    /**
     * Remove one or more values from an array.
     *
430 431
     * @param  string  $column
     * @param  mixed   $values
432 433
     * @return mixed
     */
434
    public function pull($column, $values)
435
    {
436
        // Do batch pull by default.
Jens Segers's avatar
Jens Segers committed
437 438 439
        if (! is_array($values)) {
            $values = [$values];
        }
440

441 442
        $query = $this->setKeysForSaveQuery($this->newQuery());

443 444 445 446 447 448 449 450 451 452 453 454 455 456
        $this->pullAttributeValues($column, $values);

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

    /**
     * Append one or more values to the underlying attribute value and sync with original.
     *
     * @param  string  $column
     * @param  array   $values
     * @param  bool    $unique
     */
    protected function pushAttributeValues($column, array $values, $unique = false)
    {
457
        $current = $this->getAttributeFromArray($column) ?: [];
458

Jens Segers's avatar
Jens Segers committed
459
        foreach ($values as $value) {
460
            // Don't add duplicate values when we only want unique values.
Jens Segers's avatar
Jens Segers committed
461 462 463
            if ($unique and in_array($value, $current)) {
                continue;
            }
464 465 466 467 468 469 470 471 472 473

            array_push($current, $value);
        }

        $this->attributes[$column] = $current;

        $this->syncOriginalAttribute($column);
    }

    /**
474
     * Remove one or more values to the underlying attribute value and sync with original.
475 476 477 478 479 480
     *
     * @param  string  $column
     * @param  array   $values
     */
    protected function pullAttributeValues($column, array $values)
    {
481
        $current = $this->getAttributeFromArray($column) ?: [];
482

Jens Segers's avatar
Jens Segers committed
483
        foreach ($values as $value) {
484 485
            $keys = array_keys($current, $value);

Jens Segers's avatar
Jens Segers committed
486
            foreach ($keys as $key) {
487 488 489 490
                unset($current[$key]);
            }
        }

Jens Segers's avatar
Jens Segers committed
491
        $this->attributes[$column] = array_values($current);
492 493

        $this->syncOriginalAttribute($column);
494 495
    }

496 497 498
    /**
     * Set the parent relation.
     *
499
     * @param  \Illuminate\Database\Eloquent\Relations\Relation  $relation
500
     */
501
    public function setParentRelation(Relation $relation)
502
    {
503
        $this->parentRelation = $relation;
504 505 506 507 508
    }

    /**
     * Get the parent relation.
     *
509
     * @return \Illuminate\Database\Eloquent\Relations\Relation
510
     */
511
    public function getParentRelation()
512
    {
513
        return $this->parentRelation;
514 515
    }

516 517 518 519 520 521 522 523 524 525 526
    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param  \Jenssegers\Mongodb\Query\Builder $query
     * @return \Jenssegers\Mongodb\Eloquent\Builder|static
     */
    public function newEloquentBuilder($query)
    {
        return new Builder($query);
    }

527 528 529 530 531 532 533 534 535 536 537
    /**
     * Get a new query builder instance for the connection.
     *
     * @return Builder
     */
    protected function newBaseQueryBuilder()
    {
        $connection = $this->getConnection();

        return new QueryBuilder($connection, $connection->getPostProcessor());
    }
538 539 540 541 542 543 544 545 546 547 548
    
    /**
     * We just return original key here in order to support keys in dot-notation
     *
     * @param  string  $key
     * @return string
     */
    protected function removeTableFromKey($key)
    {
        return $key;
    }
549

Jens Segers's avatar
Jens Segers committed
550 551 552 553 554 555 556 557 558
    /**
     * Handle dynamic method calls into the method.
     *
     * @param  string  $method
     * @param  array   $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
unknown's avatar
unknown committed
559
        // Unset method
Jens Segers's avatar
Jens Segers committed
560
        if ($method == 'unset') {
561
            return call_user_func_array([$this, 'drop'], $parameters);
Jens Segers's avatar
Jens Segers committed
562 563 564 565
        }

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