Completed
Push — master ( 2b2ac0...ca32f1 )
by Oscar
03:02
created

Row::edit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace SimpleCrud;
4
5
use SimpleCrud\Scheme\Scheme;
6
7
/**
8
 * Stores the data of an table row.
9
 */
10
class Row extends AbstractRow
11
{
12
    private $values = [];
13
    private $relations = [];
14
    private $changed = false;
15
16
    /**
17
     * {@inheritdoc}
18
     */
19
    public function __construct(Table $table)
20
    {
21
        parent::__construct($table);
22
23
        foreach ($table->getScheme()['fields'] as $name => $field) {
24
            $this->values[$name] = null;
25
        }
26
    }
27
28
    /**
29
     * Debug info.
30
     * 
31
     * @return array
32
     */
33
    public function __debugInfo()
34
    {
35
        return [
36
            'table' => $this->getTable()->name,
37
            'values' => $this->values,
38
        ];
39
    }
40
41
    /**
42
     * Clear the current cache.
43
     */
44
    public function clearCache()
45
    {
46
        $this->relations = [];
47
    }
48
49
    /**
50
     * Magic method to return properties or load them automatically.
51
     *
52
     * @param string $name
53
     */
54
    public function __get($name)
55
    {
56
        //It's a field
57
        if (array_key_exists($name, $this->values)) {
58
            return $this->values[$name];
59
        }
60
61
        //It's a relation
62
        if (array_key_exists($name, $this->relations)) {
63
            return $this->relations[$name] ?: new NullValue();
64
        }
65
66
        //Load the relation
67
        $scheme = $this->getTable()->getScheme();
68
69
        if (isset($scheme['relations'][$name])) {
70
            return ($this->relations[$name] = call_user_func([$this, $name])->run()) ?: new NullValue();
71
        }
72
73
        //Exists as a function
74
        if (method_exists($this, $name)) {
75
            return $this->$name();
76
        }
77
78
        throw new SimpleCrudException(sprintf('Undefined property "%s"', $name));
79
    }
80
81
    /**
82
     * Magic method to store properties.
83
     *
84
     * @param string $name
85
     * @param mixed  $value
86
     */
87
    public function __set($name, $value)
88
    {
89
        //It's a field
90
        if (array_key_exists($name, $this->values)) {
91
            if ($this->values[$name] !== $value) {
92
                $this->changed = true;
93
            }
94
95
            return $this->values[$name] = $value;
96
        }
97
98
        //It's a relation
99
        $table = $this->getTable();
100
        $scheme = $table->getScheme();
101
102
        if (!isset($scheme['relations'][$name])) {
103
            throw new SimpleCrudException(sprintf('Undefined property "%s"', $name));
104
        }
105
106
        $relation = $scheme['relations'][$name][0];
107
108
        //Check types
109
        if ($relation === Scheme::HAS_ONE) {
110
            if ($value !== null && !($value instanceof self)) {
111
                throw new SimpleCrudException(sprintf('Invalid value: %s must be a Row instance or null', $name));
112
            }
113
        } elseif (!($value instanceof RowCollection)) {
114
            throw new SimpleCrudException(sprintf('Invalid value: %s must be a RowCollection', $name));
115
        }
116
117
        $this->relations[$name] = $value;
118
    }
119
120
    /**
121
     * Magic method to check if a property is defined or not.
122
     *
123
     * @param string $name Property name
124
     *
125
     * @return bool
126
     */
127
    public function __isset($name)
128
    {
129
        return isset($this->values[$name]) || isset($this->relations[$name]);
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function toArray(array $bannedEntities = [])
136
    {
137
        $table = $this->getTable();
138
139
        if (!empty($bannedEntities) && in_array($table->name, $bannedEntities)) {
140
            return;
141
        }
142
143
        $bannedEntities[] = $table->name;
144
        $data = $this->values;
145
146
        foreach ($this->relations as $name => $value) {
147
            if ($value !== null) {
148
                $data[$name] = $value->toArray($bannedEntities);
149
            }
150
        }
151
152
        return $data;
153
    }
154
155
    public function edit(array $values)
156
    {
157
        foreach ($values as $name => $value) {
158
            $this->__set($name, $value);
159
        }
160
161
        return $this;
162
    }
163
164
    /**
165
     * Saves this row in the database.
166
     *
167
     * @param bool|array $relations Set true to save the relations with other entities
168
     *
169
     * @return $this
170
     */
171
    public function save($relations = false)
172
    {
173
        if ($relations === true) {
174
            $relations = array_keys($this->relations);
175
        }
176
177
        if ($relations) {
178
            $scheme = $this->getTable()->getScheme()['relations'];
179
180
            foreach ($relations as $name) {
0 ignored issues
show
Bug introduced by
The expression $relations of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
181
                if (!array_key_exists($name, $this->relations)) {
182
                    continue;
183
                }
184
185
                if (!isset($scheme[$name])) {
186
                    throw new SimpleCrudException(sprintf('Invalid relation: %s', $name));
187
                }
188
189
                $relation = $scheme[$name];
190
191
                if ($relation[0] === Scheme::HAS_ONE) {
192
                    $this->{$relation[1]} = ($this->relations[$name] === null) ? null : $this->relations[$name]->save()->id;
193
                }
194
            }
195
        }
196
197
        if ($this->changed) {
198
            if (empty($this->id)) {
199
                $this->id = $this->table->insert()
0 ignored issues
show
Documentation Bug introduced by
The method insert does not exist on object<SimpleCrud\Table>? 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...
200
                    ->data($this->values)
201
                    ->run();
202
            } else {
203
                $this->table->update()
0 ignored issues
show
Documentation Bug introduced by
The method update does not exist on object<SimpleCrud\Table>? 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...
204
                    ->data($this->values)
205
                    ->byId($this->id)
206
                    ->limit(1)
207
                    ->run();
208
            }
209
210
            $this->table->cache($this);
211
        }
212
213
        if ($relations) {
214
            $scheme = $this->getTable()->getScheme()['relations'];
215
216
            foreach ($relations as $name) {
0 ignored issues
show
Bug introduced by
The expression $relations of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
217
                if (!array_key_exists($name, $this->relations)) {
218
                    continue;
219
                }
220
221
                if (!isset($scheme[$name])) {
222
                    throw new SimpleCrudException(sprintf('Invalid relation: %s', $name));
223
                }
224
225
                $relation = $scheme[$name];
226
227
                if ($relation[0] === Scheme::HAS_MANY) {
228
                    foreach ($this->relations[$name] as $row) {
229
                        $row->{$relation[1]} = $this->id;
230
                        $row->save();
231
                    }
232
233
                    continue;
234
                }
235
236
                if ($relation[0] === Scheme::HAS_MANY_TO_MANY) {
237
                    $bridge = $this->getDatabase()->{$relation[1]};
238
239
                    $bridge
240
                        ->delete()
241
                        ->by($relation[2], $this->id)
242
                        ->run();
243
244
                    foreach ($this->relations[$name] as $row) {
245
                        $bridge
246
                            ->insert()
247
                            ->data([
248
                                $relation[2] => $this->id,
249
                                $relation[3] => $row->id,
250
                            ])
251
                            ->run();
252
                    }
253
                }
254
            }
255
        }
256
257
        return $this;
258
    }
259
260
    /**
261
     * Join this row with other row
262
     *
263
     * @param Row $row
264
     *
265
     * @return $this
266
     */
267
    public function join(Row $row)
268
    {
269
        $table = $this->getTable();
270
        $relationTable = $row->getTable();
271
        $relations = $table->getScheme()['relations'];
272
273 View Code Duplication
        if (!isset($relations[$relationTable->name])) {
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...
274
            throw new SimpleCrudException(sprintf('Invalid relation: %s - %s', $table->name, $relationTable->name));
275
        }
276
277
        $relation = $relations[$relationTable->name];
278
279
        if ($relation[0] === Scheme::HAS_ONE) {
280
            $this->{$relation[1]} = $row->id;
281
            return $this->save();
282
        }
283
284
        if ($relation[0] === Scheme::HAS_MANY) {
285
            if ($this->id === null) {
286
                $this->save();
287
            }
288
289
            $row->{$relation[1]} = $this->id;
290
            $row->save();
291
            return $this;
292
        }
293
294
        if ($relation[0] === Scheme::HAS_MANY_TO_MANY) {
295
            $bridge = $this->getDatabase()->{$relation[1]};
296
297
            $bridge
298
                ->insert()
299
                ->duplications()
300
                ->data([
301
                    $relation[2] => $this->id,
302
                    $relation[3] => $row->id,
303
                ])
304
                ->run();
305
        }
306
    }
307
}
308