Completed
Push — master ( 3a177f...0ba58c )
by Gabriel
03:15
created

HasRelationsRecordsTrait::morphTo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
3
namespace Nip\Records\Traits\Relations;
4
5
use Nip\Records\Record;
6
use Nip\Records\Relations\MorphToMany;
7
use Nip\Records\Relations\Relation;
8
use Nip\Records\Traits\AbstractTrait\RecordsTrait;
9
10
/**
11
 * Trait HasRelationsRecordsTrait
12
 * @package Nip\Records\Traits\Relations
13
 */
14
trait HasRelationsRecordsTrait
15
{
16
    use RecordsTrait;
17
18
    /**
19
     * The loaded relationships for the model table.
20
     * @var Relation[]
21
     */
22
    protected $relations = null;
23
24
    protected $relationTypes = ['belongsTo', 'hasMany', 'hasAndBelongsToMany'];
25
26
27
    /**
28
     * Get a specified relationship.
29
     * @param  string $relation
30
     * @return null|Relation
31
     * @throws \Exception
32
     */
33 1
    public function getRelation($relation)
34
    {
35 1
        $this->checkInitRelations();
36
37 1
        return $this->relations[$relation];
38
    }
39
40
    /**
41
     * Check if the model needs to initRelations
42
     * @return void
43
     * @throws \Exception
44
     */
45 1
    protected function checkInitRelations()
46
    {
47 1
        if ($this->relations === null) {
48
            $this->initRelations();
49
        }
50 1
    }
51
52
    /**
53
     * @throws \Exception
54
     */
55
    protected function initRelations()
56
    {
57
        $this->relations = [];
58
        foreach ($this->relationTypes as $type) {
59
            $this->initRelationsType($type);
60
        }
61
    }
62
63
    /**
64
     * @param string $type
65
     * @throws \Exception
66
     */
67
    protected function initRelationsType($type)
68
    {
69
        if (property_exists($this, '_' . $type)) {
70
            $array = $this->{'_' . $type};
71
            $this->initRelationsFromArray($type, $array);
72
        }
73
    }
74
75
    /**
76
     * @param string $type
77
     * @param $array
78
     * @throws \Exception
79
     */
80
    public function initRelationsFromArray($type, $array)
81
    {
82
        foreach ($array as $key => $item) {
83
            $name = is_array($item) ? $key : $item;
84
            $params = is_array($item) ? $item : [];
85
            $this->initRelation($type, $name, $params);
86
        }
87
    }
88
89
    /**
90
     * @param string $type
91
     * @param string $name
92
     * @param array $params
93
     * @return Relation
94
     * @throws \Exception
95
     */
96 1
    protected function initRelation($type, $name, $params)
97
    {
98 1
        $relation = $this->newRelation($type);
99 1
        $relation->setName($name);
100 1
        $relation->addParams($params);
101
102 1
        $this->relations[$name] = $relation;
103
104 1
        return $relation;
105
    }
106
107
    /**
108
     * @param string $type
109
     * @return \Nip\Records\Relations\Relation
110
     */
111 2
    public function newRelation($type)
112
    {
113 2
        $class = $this->getRelationClass($type);
114
        /** @var \Nip\Records\Relations\Relation $relation */
115 2
        $relation = new $class();
116 2
        $relation->setManager($this);
117
118 2
        return $relation;
119
    }
120
121
    /**
122
     * @param string $type
123
     * @return string
124
     */
125 3
    public function getRelationClass($type)
126
    {
127 3
        $class = 'Nip\Records\Relations\\' . ucfirst($type);
128
129 3
        return $class;
130
    }
131
132
    /**
133
     * @param $name
134
     * @param array $params
135
     * @return Relation
136
     * @throws \Exception
137
     */
138 1
    public function belongsTo($name, $params = [])
139
    {
140 1
        return $this->initRelation('belongsTo', $name, $params);
141
    }
142
143
    /**
144
     * @param $name
145
     * @param array $params
146
     * @return Relation
147
     * @throws \Exception
148
     */
149
    public function hasMany($name, $params = [])
150
    {
151
        return $this->initRelation('hasMany', $name, $params);
152
    }
153
154
    /** @noinspection PhpMethodNamingConventionInspection
155
     * @param $name
156
     * @param array $params
157
     * @return Relation
158
     * @throws \Exception
159
     */
160
    public function HABTM($name, $params = [])
161
    {
162
        return $this->initRelation('hasAndBelongsToMany', $name, $params);
163
    }
164
165
    /**
166
     * @param $name
167
     * @param array $params
168
     * @return Relation
169
     * @throws \Exception
170
     */
171
    public function morphTo($name, $params = [])
172
    {
173
        return $this->initRelation('morphTo', $name, $params);
174
    }
175
176
    /**
177
     * @param $name
178
     * @param array $params
179
     * @return Relation
180
     * @throws \Exception
181
     */
182
    public function morphMany($name, $params = [])
183
    {
184
        return $this->initRelation('morphMany', $name, $params);
185
    }
186
187
    /**
188
     * @param $name
189
     * @param array $params
190
     * @return Relation
191
     * @throws \Exception
192
     */
193
    public function morphToMany($name, $params = [])
194
    {
195
        return $this->initRelation('morphToMany', $name, $params);
196
    }
197
198
    /**
199
     * @param $name
200
     * @param array $params
201
     * @return MorphToMany
202
     * @throws \Exception
203
     */
204
    public function morphedByMany($name, $params = [])
205
    {
206
        /** @var MorphToMany $relation */
207
        $relation = $this->initRelation('morphToMany', $name, $params);
208
        $relation->setInverse(true);
209
        return $relation;
210
    }
211
212
    /**
213
     * Determine if the given relation is loaded.
214
     * @param  string $key
215
     * @return bool
216
     * @throws \Exception
217
     */
218
    public function hasRelation($key)
219
    {
220
        $this->checkInitRelations();
221
222
        return array_key_exists($key, $this->relations);
223
    }
224
225
    /**
226
     * Set the specific relationship in the model.
227
     * @param  string $relation
228
     * @param  mixed $value
229
     * @return $this
230
     * @throws \Exception
231
     */
232
    public function setRelation($relation, $value)
233
    {
234
        $this->checkInitRelations();
235
        $this->relations[$relation] = $value;
236
237
        return $this;
238
    }
239
240
    /**
241
     * @param HasRelationsRecordTrait $from
0 ignored issues
show
introduced by
The type HasRelationsRecordTrait for parameter $from is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
242
     * @param HasRelationsRecordTrait $to
0 ignored issues
show
introduced by
The type HasRelationsRecordTrait for parameter $to is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?
Loading history...
243
     * @return HasRelationsRecordTrait
0 ignored issues
show
Comprehensibility Bug introduced by
The return type HasRelationsRecordTrait is a trait, and thus cannot be used for type-hinting in PHP. Maybe consider adding an interface and use that for type-hinting?

In PHP traits cannot be used for type-hinting as they do not define a well-defined structure. This is because any class that uses a trait can rename that trait’s methods.

If you would like to return an object that has a guaranteed set of methods, you could create a companion interface that lists these methods explicitly.

Loading history...
244
     * @throws \Exception
245
     */
246
    public function cloneRelations($from, $to)
247
    {
248
        $relations = $from->getManager()->getRelations();
0 ignored issues
show
Documentation Bug introduced by
The method getRelations does not exist on object<Nip\Records\AbstractModels\RecordManager>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
249
        foreach ($relations as $name => $relation) {
250
            /** @var \Nip\Records\Relations\HasMany $relation */
251
            if ($relation->getType() != 'belongsTo') {
252
                /** @var Record[] $associatedOld */
253
                $associatedOld = $from->{'get' . $name}();
254
                if (count($associatedOld)) {
255
                    $associatedNew = $to->getRelation($name)->newCollection();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Nip\Records\Relations\Relation as the method newCollection() does only exist in the following sub-classes of Nip\Records\Relations\Relation: Nip\Records\Relations\HasAndBelongsToMany, Nip\Records\Relations\HasMany, Nip\Records\Relations\HasOneOrMany, Nip\Records\Relations\MorphMany, Nip\Records\Relations\MorphOneOrMany, Nip\Records\Relations\MorphToMany. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
256
                    foreach ($associatedOld as $associated) {
257
                        $aItem = $associated->getCloneWithRelations();
258
                        $associatedNew[] = $aItem;
259
                    }
260
                    $to->getRelation($name)->setResults($associatedNew);
261
                }
262
            }
263
        }
264
265
        return $to;
266
    }
267
268
    /**
269
     * Get all the loaded relations for the instance.
270
     * @return array
271
     * @throws \Exception
272
     */
273
    public function getRelations()
274
    {
275
        $this->checkInitRelations();
276
277
        return $this->relations;
278
    }
279
280
    /**
281
     * Set the entire relations array on the model.
282
     * @param  array $relations
283
     * @return $this
284
     */
285
    public function setRelations(array $relations)
286
    {
287
        $this->relations = $relations;
288
289
        return $this;
290
    }
291
}
292