Completed
Pull Request — master (#5)
by Jacob
03:15
created

Persister::query()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 7
1
<?php
2
3
namespace As3\Modlr\Persister\MongoDb;
4
5
use As3\Modlr\Store\Store;
6
use As3\Modlr\Models\Model;
7
use As3\Modlr\Models\Collection;
8
use As3\Modlr\Metadata\EntityMetadata;
9
use As3\Modlr\Metadata\AttributeMetadata;
10
use As3\Modlr\Metadata\RelationshipMetadata;
11
use As3\Modlr\Persister\PersisterInterface;
12
use As3\Modlr\Persister\PersisterException;
13
use As3\Modlr\Persister\Record;
14
use Doctrine\MongoDB\Connection;
15
use \MongoId;
16
17
/**
18
 * Persists and retrieves models to/from a MongoDB database.
19
 *
20
 * @author Jacob Bare <[email protected]>
21
 */
22
final class Persister implements PersisterInterface
23
{
24
    const IDENTIFIER_KEY    = '_id';
25
    const POLYMORPHIC_KEY   = '_type';
26
    const PERSISTER_KEY     = 'mongodb';
27
28
    /**
29
     * The Doctine MongoDB connection.
30
     *
31
     * @var Connection
32
     */
33
    private $connection;
34
35
    /**
36
     * The query/database operations formatter.
37
     *
38
     * @var Formatter
39
     */
40
    private $formatter;
41
42
    /**
43
     * @var StorageMetadataFactory
44
     */
45
    private $smf;
46
47
    /**
48
     * Constructor.
49
     *
50
     * @param   Connection              $connection
51
     * @param   StorageMetadataFactory  $smf
52
     */
53
    public function __construct(Connection $connection, StorageMetadataFactory $smf)
54
    {
55
        $this->connection = $connection;
56
        $this->formatter = new Formatter();
57
        $this->smf = $smf;
58
59
    }
60
61
    /**
62
     * {@inheritDoc}
63
     */
64
    public function getPersisterKey()
65
    {
66
        return self::PERSISTER_KEY;
67
    }
68
69
    /**
70
     * {@inheritDoc}
71
     */
72
    public function getPersistenceMetadataFactory()
73
    {
74
        return $this->smf;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->smf; (As3\Modlr\Persister\MongoDb\StorageMetadataFactory) is incompatible with the return type declared by the interface As3\Modlr\Persister\Pers...sistenceMetadataFactory of type As3\Modlr\Metadata\Inter...etadataFactoryInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
75
    }
76
77
    /**
78
     * {@inheritDoc}
79
     * @todo    Implement sorting and pagination (limit/skip).
80
     */
81
    public function all(EntityMetadata $metadata, Store $store, array $identifiers = [])
82
    {
83
        $criteria = $this->getRetrieveCritiera($metadata, $identifiers);
84
        $cursor = $this->doQuery($metadata, $store, $criteria);
85
        return $this->hydrateRecords($metadata, $cursor->toArray(), $store);
86
    }
87
88
    /**
89
     * {@inheritDoc}
90
     */
91
    public function query(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
92
    {
93
        $cursor = $this->doQuery($metadata, $store, $criteria);
94
        return $this->hydrateRecords($metadata, $cursor->toArray(), $store);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->hydrateRec...or->toArray(), $store); (As3\Modlr\Persister\Record[]) is incompatible with the return type declared by the interface As3\Modlr\Persister\PersisterInterface::query of type As3\Modlr\Persister\Collection.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
95
    }
96
97
    /**
98
     * {@inheritDoc}
99
     */
100
    public function inverse(EntityMetadata $owner, EntityMetadata $rel, Store $store, array $identifiers, $inverseField)
101
    {
102
        $criteria = $this->getInverseCriteria($owner, $rel, $identifiers, $inverseField);
103
        $cursor = $this->doQuery($rel, $store, $criteria);
104
        return $this->hydrateRecords($rel, $cursor->toArray(), $store);
105
    }
106
107
    /**
108
     * {@inheritDoc}
109
     */
110
    public function retrieve(EntityMetadata $metadata, $identifier, Store $store)
111
    {
112
        $criteria = $this->getRetrieveCritiera($metadata, $identifier);
113
        $result = $this->doQuery($metadata, $store, $criteria)->getSingleResult();
114
        if (null === $result) {
115
            return;
116
        }
117
        return $this->hydrateRecord($metadata, $result, $store);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->doQuery($metadata...ria)->getSingleResult() on line 113 can also be of type object; however, As3\Modlr\Persister\Mong...sister::hydrateRecord() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
118
    }
119
120
    /**
121
     * {@inheritDoc}
122
     * @todo    Optimize the changeset to query generation.
123
     */
124
    public function create(Model $model)
125
    {
126
        $metadata = $model->getMetadata();
127
        $insert[$this->getIdentifierKey()] = $this->convertId($model->getId());
0 ignored issues
show
Coding Style Comprehensibility introduced by
$insert was never initialized. Although not strictly required by PHP, it is generally a good practice to add $insert = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
128
        if (true === $metadata->isChildEntity()) {
129
            $insert[$this->getPolymorphicKey()] = $metadata->type;
130
        }
131
132
        $changeset = $model->getChangeSet();
133 View Code Duplication
        foreach ($changeset['attributes'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
134
            $value = $this->prepareAttribute($metadata->getAttribute($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getAttribute($key) can be null; however, prepareAttribute() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ter::prepareAttribute() has been deprecated.

This method has been deprecated.

Loading history...
135
            if (null === $value) {
136
                continue;
137
            }
138
            $insert[$key] = $value;
139
        }
140 View Code Duplication
        foreach ($changeset['hasOne'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
141
            $value = $this->prepareHasOne($metadata->getRelationship($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getRelationship($key) can be null; however, prepareHasOne() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...sister::prepareHasOne() has been deprecated.

This method has been deprecated.

Loading history...
142
            if (null === $value) {
143
                continue;
144
            }
145
            $insert[$key] = $value;
146
        }
147 View Code Duplication
        foreach ($changeset['hasMany'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
148
            $value = $this->prepareHasMany($metadata->getRelationship($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getRelationship($key) can be null; however, prepareHasMany() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ister::prepareHasMany() has been deprecated.

This method has been deprecated.

Loading history...
149
            if (null === $value) {
150
                continue;
151
            }
152
            $insert[$key] = $value;
153
        }
154
        $this->createQueryBuilder($metadata)
155
            ->insert()
156
            ->setNewObj($insert)
157
            ->getQuery()
158
            ->execute()
159
        ;
160
        return $model;
161
    }
162
163
    /**
164
     * Prepares and formats an attribute value for proper insertion into the database.
165
     *
166
     * @deprecated
167
     * @param   AttributeMetadata   $attrMeta
168
     * @param   mixed               $value
169
     * @return  mixed
170
     */
171 View Code Duplication
    protected function prepareAttribute(AttributeMetadata $attrMeta, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
172
    {
173
        // Handle data type conversion, if needed.
174
        if ('date' === $attrMeta->dataType && $value instanceof \DateTime) {
175
            return new \MongoDate($value->getTimestamp(), $value->format('u'));
176
        }
177
        return $value;
178
    }
179
180
    /**
181
     * Prepares and formats a has-one relationship model for proper insertion into the database.
182
     *
183
     * @deprecated
184
     * @param   RelationshipMetadata    $relMeta
185
     * @param   Model|null              $model
186
     * @return  mixed
187
     */
188 View Code Duplication
    protected function prepareHasOne(RelationshipMetadata $relMeta, Model $model = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
189
    {
190
        if (null === $model || true === $relMeta->isInverse) {
191
            return null;
192
        }
193
        return $this->createReference($relMeta, $model);
0 ignored issues
show
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ster::createReference() has been deprecated.

This method has been deprecated.

Loading history...
194
    }
195
196
    /**
197
     * Prepares and formats a has-many relationship model set for proper insertion into the database.
198
     *
199
     * @deprecated
200
     * @param   RelationshipMetadata    $relMeta
201
     * @param   Model[]|null            $models
202
     * @return  mixed
203
     */
204 View Code Duplication
    protected function prepareHasMany(RelationshipMetadata $relMeta, array $models = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
205
    {
206
        if (null === $models || true === $relMeta->isInverse) {
207
            return null;
208
        }
209
        $references = [];
210
        foreach ($models as $model) {
211
            $references[] = $this->createReference($relMeta, $model);
0 ignored issues
show
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ster::createReference() has been deprecated.

This method has been deprecated.

Loading history...
212
        }
213
        return empty($references) ? null : $references;
214
    }
215
216
    /**
217
     * Creates a reference for storage of a related model in the database
218
     *
219
     * @deprecated
220
     * @param   RelationshipMetadata    $relMeta
221
     * @param   Model                   $model
222
     * @return  mixed
223
     */
224
    protected function createReference(RelationshipMetadata $relMeta, Model $model)
225
    {
226 View Code Duplication
        if (true === $relMeta->isPolymorphic()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
227
            $reference[$this->getIdentifierKey()] = $this->convertId($model->getId());
0 ignored issues
show
Coding Style Comprehensibility introduced by
$reference was never initialized. Although not strictly required by PHP, it is generally a good practice to add $reference = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
228
            $reference[$this->getPolymorphicKey()] = $model->getType();
229
            return $reference;
230
        }
231
        return $this->convertId($model->getId());
232
    }
233
234
    /**
235
     * {@inheritDoc}
236
     * @todo    Optimize the changeset to query generation.
237
     */
238
    public function update(Model $model)
239
    {
240
        $metadata = $model->getMetadata();
241
        $criteria = $this->getRetrieveCritiera($metadata, $model->getId());
242
        $changeset = $model->getChangeSet();
243
244
        $update = [];
245 View Code Duplication
        foreach ($changeset['attributes'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
            if (null === $values['new']) {
247
                $op = '$unset';
248
                $value = 1;
249
            } else {
250
                $op = '$set';
251
                $value = $this->prepareAttribute($metadata->getAttribute($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getAttribute($key) can be null; however, prepareAttribute() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ter::prepareAttribute() has been deprecated.

This method has been deprecated.

Loading history...
252
            }
253
            $update[$op][$key] = $value;
254
        }
255
256
        // @todo Must prevent inverse relationships from persisting
257 View Code Duplication
        foreach ($changeset['hasOne'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
258
            if (null === $values['new']) {
259
                $op = '$unset';
260
                $value = 1;
261
            } else {
262
                $op = '$set';
263
                $value = $this->prepareHasOne($metadata->getRelationship($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getRelationship($key) can be null; however, prepareHasOne() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...sister::prepareHasOne() has been deprecated.

This method has been deprecated.

Loading history...
264
            }
265
            $update[$op][$key] = $value;
266
        }
267
268 View Code Duplication
        foreach ($changeset['hasMany'] as $key => $values) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
269
            if (null === $values['new']) {
270
                $op = '$unset';
271
                $value = 1;
272
            } else {
273
                $op = '$set';
274
                $value = $this->prepareHasMany($metadata->getRelationship($key), $values['new']);
0 ignored issues
show
Bug introduced by
It seems like $metadata->getRelationship($key) can be null; however, prepareHasMany() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Deprecated Code introduced by
The method As3\Modlr\Persister\Mong...ister::prepareHasMany() has been deprecated.

This method has been deprecated.

Loading history...
275
            }
276
            $update[$op][$key] = $value;
277
        }
278
279
        if (empty($update)) {
280
            return $model;
281
        }
282
283
        $this->createQueryBuilder($metadata)
284
            ->update()
285
            ->setQueryArray($criteria)
286
            ->setNewObj($update)
287
            ->getQuery()
288
            ->execute();
289
        ;
290
        return $model;
291
    }
292
293
    /**
294
     * {@inheritDoc}
295
     */
296
    public function delete(Model $model)
297
    {
298
        $metadata = $model->getMetadata();
299
        $criteria = $this->getRetrieveCritiera($metadata, $model->getId());
300
301
        $this->createQueryBuilder($metadata)
302
            ->remove()
303
            ->setQueryArray($criteria)
304
            ->getQuery()
305
            ->execute();
306
        ;
307
        return $model;
308
    }
309
310
    /**
311
     * {@inheritDoc}
312
     */
313
    public function generateId($strategy = null)
314
    {
315
        if (false === $this->getFormatter()->isIdStrategySupported($strategy)) {
316
            throw PersisterException::nyi('ID generation currently only supports an object strategy, or none at all.');
317
        }
318
        return new MongoId();
319
    }
320
321
    /**
322
     * @return  Formatter
323
     */
324
    public function getFormatter()
325
    {
326
        return $this->formatter;
327
    }
328
329
    /**
330
     * {@inheritDoc}
331
     */
332
    public function convertId($identifier, $strategy = null)
333
    {
334
        return $this->getFormatter()->getIdentifierDbValue($identifier, $strategy);
335
    }
336
337
    /**
338
     * {@inheritDoc}
339
     */
340
    public function getIdentifierKey()
341
    {
342
        return self::IDENTIFIER_KEY;
343
    }
344
345
    /**
346
     * {@inheritDoc}
347
     */
348
    public function getPolymorphicKey()
349
    {
350
        return self::POLYMORPHIC_KEY;
351
    }
352
353
    /**
354
     * {@inheritDoc}
355
     */
356
    public function extractType(EntityMetadata $metadata, array $data)
357
    {
358
        if (false === $metadata->isPolymorphic()) {
359
            return $metadata->type;
360
        }
361
        if (!isset($data[$this->getPolymorphicKey()])) {
362
            throw PersisterException::badRequest(sprintf('Unable to extract polymorphic type. The "%s" key was not found.', $this->getPolymorphicKey()));
363
        }
364
        return $data[$this->getPolymorphicKey()];
365
    }
366
367
    /**
368
     * Finds records from the database based on the provided metadata and criteria.
369
     *
370
     * @param   EntityMetadata  $metadata   The model metadata that the database should query against.
371
     * @param   Store           $store      The store.
372
     * @param   array           $criteria   The query criteria.
373
     * @param   array           $fields     Fields to include/exclude.
374
     * @param   array           $sort       The sort criteria.
375
     * @param   int             $offset     The starting offset, aka the number of Models to skip.
376
     * @param   int             $limit      The number of Models to limit.
377
     * @return  \Doctrine\MongoDB\Cursor
378
     */
379
    protected function doQuery(EntityMetadata $metadata, Store $store, array $criteria, array $fields = [], array $sort = [], $offset = 0, $limit = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $fields is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $sort is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $offset is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $limit is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
380
    {
381
        $criteria = $this->getFormatter()->formatQuery($metadata, $store, $criteria);
382
        return $this->createQueryBuilder($metadata)
383
            ->find()
384
            ->setQueryArray($criteria)
385
            ->getQuery()
386
            ->execute()
387
        ;
388
    }
389
390
    /**
391
     * Processes multiple, raw MongoDB results an converts them into an array of standardized Record objects.
392
     *
393
     * @param   EntityMetadata  $metadata
394
     * @param   array           $results
395
     * @param   Store           $store
396
     * @return  Record[]
397
     */
398
    protected function hydrateRecords(EntityMetadata $metadata, array $results, Store $store)
399
    {
400
        $records = [];
401
        foreach ($results as $data) {
402
            $records[] = $this->hydrateRecord($metadata, $data, $store);
403
        }
404
        return $records;
405
    }
406
407
    /**
408
     * Processes raw MongoDB data an converts it into a standardized Record object.
409
     *
410
     * @param   EntityMetadata  $metadata
411
     * @param   array           $data
412
     * @param   Store           $store
413
     * @return  Record
414
     */
415
    protected function hydrateRecord(EntityMetadata $metadata, array $data, Store $store)
416
    {
417
        $identifier = $data[$this->getIdentifierKey()];
418
        unset($data[$this->getIdentifierKey()]);
419
420
        $type = $this->extractType($metadata, $data);
421
        unset($data[$this->getPolymorphicKey()]);
422
423
        $metadata = $store->getMetadataForType($type);
424
        foreach ($metadata->getRelationships() as $key => $relMeta) {
425
            if (!isset($data[$key])) {
426
                continue;
427
            }
428
            if (true === $relMeta->isMany() && !is_array($data[$key])) {
429
                throw PersisterException::badRequest(sprintf('Relationship key "%s" is a reference many. Expected record data type of array, "%s" found on model "%s" for identifier "%s"', $key, gettype($data[$key]), $type, $identifier));
430
            }
431
            $references = $relMeta->isOne() ? [$data[$key]] : $data[$key];
432
433
            $extracted = [];
434
            foreach ($references as $reference) {
435
                $extracted[] =  $this->extractRelationship($relMeta, $reference);
436
            }
437
            $data[$key] = $relMeta->isOne() ? reset($extracted) : $extracted;
438
        }
439
        return new Record($type, $identifier, $data);
440
    }
441
442
    /**
443
     * Extracts a standard relationship array that the store expects from a raw MongoDB reference value.
444
     *
445
     * @param   RelationshipMetadata    $relMeta
446
     * @param   mixed                   $reference
447
     * @return  array
448
     * @throws  \RuntimeException   If the relationship could not be extracted.
449
     */
450
    protected function extractRelationship(RelationshipMetadata $relMeta, $reference)
451
    {
452
        $simple = false === $relMeta->isPolymorphic();
453
        $idKey = $this->getIdentifierKey();
454
        $typeKey = $this->getPolymorphicKey();
455
        if (true === $simple && is_array($reference) && isset($reference[$idKey])) {
456
            return [
457
                'id'    => $reference[$idKey],
458
                'type'  => $relMeta->getEntityType(),
459
            ];
460
        } elseif (true === $simple && !is_array($reference)) {
461
            return [
462
                'id'    => $reference,
463
                'type'  => $relMeta->getEntityType(),
464
            ];
465
        } elseif (false === $simple && is_array($reference) && isset($reference[$idKey]) && isset($reference[$typeKey])) {
466
            return [
467
                'id'    => $reference[$idKey],
468
                'type'  => $reference[$typeKey],
469
            ];
470
        } else {
471
            throw new RuntimeException('Unable to extract a reference id.');
472
        }
473
    }
474
475
    /**
476
     * Gets standard database retrieval criteria for an inverse relationship.
477
     *
478
     * @param   EntityMetadata  $metadata       The entity to retrieve database records for.
0 ignored issues
show
Bug introduced by
There is no parameter named $metadata. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
479
     * @param   string|array    $identifiers    The IDs to query.
480
     * @return  array
481
     */
482
    protected function getInverseCriteria(EntityMetadata $owner, EntityMetadata $related, $identifiers, $inverseField)
483
    {
484
        $criteria[$inverseField] = $this->getIdentifierCriteria($identifiers);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$criteria was never initialized. Although not strictly required by PHP, it is generally a good practice to add $criteria = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
Bug introduced by
The method getIdentifierCriteria() does not seem to exist on object<As3\Modlr\Persister\MongoDb\Persister>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
485
        if (true === $owner->isChildEntity()) {
486
            // The owner is owned by a polymorphic model. Must include the type with the inverse field criteria.
487
            $criteria[$inverseField] = [
488
                $this->getIdentifierKey()   => $criteria[$inverseField],
489
                $this->getPolymorphicKey()  => $owner->type,
490
            ];
491
        }
492
        if (true === $related->isChildEntity()) {
493
            // The relationship is owned by a polymorphic model. Must include the type in the root criteria.
494
            $criteria[$this->getPolymorphicKey()] = $related->type;
495
        }
496
        return $criteria;
497
    }
498
499
    /**
500
     * Gets standard database retrieval criteria for an entity and the provided identifiers.
501
     *
502
     * @param   EntityMetadata      $metadata       The entity to retrieve database records for.
503
     * @param   string|array|null   $identifiers    The IDs to query.
504
     * @return  array
505
     */
506
    protected function getRetrieveCritiera(EntityMetadata $metadata, $identifiers = null)
507
    {
508
        $criteria = [];
509
        if (true === $metadata->isChildEntity()) {
510
            $criteria[$this->getPolymorphicKey()] = $metadata->type;
511
        }
512
513
        if (null === $identifiers) {
514
            return $criteria;
515
        }
516
        $identifiers = (array) $identifiers;
517
        if (empty($identifiers)) {
518
            return $criteria;
519
        }
520
        $criteria[$this->getIdentifierKey()] = (1 === count($identifiers)) ? $identifiers[0] : $identifiers;
521
        return $criteria;
522
    }
523
524
    /**
525
     * Creates a builder object for querying MongoDB based on the provided metadata.
526
     *
527
     * @param   EntityMetadata  $metadata
528
     * @return  \Doctrine\MongoDB\Query\Builder
529
     */
530
    protected function createQueryBuilder(EntityMetadata $metadata)
531
    {
532
        return $this->getModelCollection($metadata)->createQueryBuilder();
533
    }
534
535
    /**
536
     * Gets the MongoDB Collection object for a Model.
537
     *
538
     * @param   EntityMetadata  $metadata
539
     * @return  \Doctrine\MongoDB\Collection
540
     */
541
    protected function getModelCollection(EntityMetadata $metadata)
542
    {
543
        return $this->connection->selectCollection($metadata->persistence->db, $metadata->persistence->collection);
0 ignored issues
show
Bug introduced by
Accessing db on the interface As3\Modlr\Metadata\Inter...s\StorageLayerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing collection on the interface As3\Modlr\Metadata\Inter...s\StorageLayerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
544
    }
545
546
    /**
547
     * Determines if the current id strategy is supported.
548
     *
549
     * @deprecated
550
     * @param   string|null     $strategy
551
     * @return  bool
552
     */
553
    protected function isIdStrategySupported($strategy)
554
    {
555
        return (null === $strategy || 'object' === $strategy);
556
    }
557
}
558