Completed
Push — master ( 686fc1...883d2e )
by Oscar
02:32
created

Row::__get()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 21
Bugs 3 Features 0
Metric Value
c 21
b 3
f 0
dl 0
loc 25
rs 6.7272
cc 7
eloc 11
nc 7
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
        $defaults = [];
24
25
        foreach ($table->getScheme()['fields'] as $name => $field) {
26
            $defaults[$name] = $field['default'];
27
        }
28
29
        $this->init($defaults);
30
    }
31
32
    /**
33
     * Clear the current cache.
34
     */
35
    public function clearCache()
36
    {
37
        $this->relations = [];
38
    }
39
40
    /**
41
     * Initialize the row with the data from database.
42
     * 
43
     * @param array $values
44
     * @param array $relations
45
     */
46
    public function init(array $values, array $relations = [])
47
    {
48
        $this->values = $values;
49
        $this->relations = $relations;
50
        $this->changed = false;
51
    }
52
53
    /**
54
     * Magic method to return properties or load them automatically.
55
     *
56
     * @param string $name
57
     */
58
    public function __get($name)
59
    {
60
        //It's a field
61
        if (array_key_exists($name, $this->values)) {
62
            return $this->values[$name];
63
        }
64
65
        //It's a relation
66
        if (array_key_exists($name, $this->relations)) {
67
            return $this->relations[$name] ?: new NullValue();
68
        }
69
70
        //Load the relation
71
        $scheme = $this->getTable()->getScheme();
72
73
        if (isset($scheme['relations'][$name])) {
74
            return ($this->relations[$name] = call_user_func([$this, $name])->run()) ?: new NullValue();
75
        }
76
77
        if (method_exists($this, $name)) {
78
            return $this->$name();
79
        }
80
81
        throw new SimpleCrudException(sprintf('Undefined property "%s"', $name));
82
    }
83
84
    /**
85
     * Magic method to store properties.
86
     *
87
     * @param string $name
88
     * @param mixed  $value
89
     */
90
    public function __set($name, $value)
91
    {
92
        //It's a field
93
        if (array_key_exists($name, $this->values)) {
94
            if ($this->values[$name] !== $value) {
95
                $this->changed = true;
96
            }
97
98
            return $this->values[$name] = $value;
99
        }
100
101
        //It's a relation
102
        $table = $this->getTable();
103
        $scheme = $table->getScheme();
104
105
        if (!isset($scheme['relations'][$name])) {
106
            throw new SimpleCrudException(sprintf('Undefined property "%s"', $name));
107
        }
108
109
        $relation = $scheme['relations'][$name][0];
110
111
        //Check types
112
        if ($relation === Scheme::HAS_ONE) {
113
            if ($value !== null && !($value instanceof self)) {
114
                throw new SimpleCrudException(sprintf('Invalid value: %s must be a Row instance or null', $name));
115
            }
116
        } elseif (!($value instanceof RowCollection)) {
117
            throw new SimpleCrudException(sprintf('Invalid value: %s must be a RowCollection', $name));
118
        }
119
120
        $this->relations[$name] = $value;
121
    }
122
123
    /**
124
     * Magic method to check if a property is defined or not.
125
     *
126
     * @param string $name Property name
127
     *
128
     * @return bool
129
     */
130
    public function __isset($name)
131
    {
132
        return isset($this->values[$name]) || isset($this->relations[$name]);
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function toArray(array $bannedEntities = [])
139
    {
140
        $table = $this->getTable();
141
142
        if (!empty($bannedEntities) && in_array($table->name, $bannedEntities)) {
143
            return;
144
        }
145
146
        $bannedEntities[] = $table->name;
147
        $data = $this->values;
148
149
        foreach ($this->relations as $name => $value) {
150
            if ($value !== null) {
151
                $data[$name] = $value->toArray($bannedEntities);
152
            }
153
        }
154
155
        return $data;
156
    }
157
158
    /**
159
     * Saves this row in the database.
160
     *
161
     * @param bool|array $relations Set true to save the relations with other entities
162
     *
163
     * @return $this
164
     */
165
    public function save($relations = false)
166
    {
167
        if ($relations === true) {
168
            $relations = array_keys($this->relations);
169
        }
170
171
        if ($relations) {
172
            $scheme = $this->getTable()->getScheme()['relations'];
173
174
            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...
175
                if (!array_key_exists($name, $this->relations)) {
176
                    continue;
177
                }
178
179
                if (!isset($scheme[$name])) {
180
                    throw new SimpleCrudException(sprintf('Invalid relation: %s', $name));
181
                }
182
183
                $relation = $scheme[$name];
184
185
                if ($relation[0] === Scheme::HAS_ONE) {
186
                    $this->{$relation[1]} = ($this->relations[$name] === null) ? null : $this->relations[$name]->save()->id;
187
                }
188
            }
189
        }
190
191
        if (!$this->changed) {
192
            return $this;
193
        }
194
195
        if (empty($this->id)) {
196
            $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...
197
                ->data($this->values)
198
                ->run();
199
        } else {
200
            $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...
201
                ->data($this->values)
202
                ->byId($this->id)
203
                ->limit(1)
204
                ->run();
205
        }
206
207
        if ($relations) {
208
            $scheme = $this->getTable()->getScheme()['relations'];
209
210
            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...
211
                if (!array_key_exists($name, $this->relations)) {
212
                    continue;
213
                }
214
215
                if (!isset($scheme[$name])) {
216
                    throw new SimpleCrudException(sprintf('Invalid relation: %s', $name));
217
                }
218
219
                $relation = $scheme[$name];
220
221
                if ($relation[0] === Scheme::HAS_MANY) {
222
                    foreach ($this->relations[$name] as $row) {
223
                        $row->{$relation[1]} = $this->id;
224
                        $row->save();
225
                    }
226
227
                    continue;
228
                }
229
230
                if ($relation[0] === Scheme::HAS_MANY_TO_MANY) {
231
                    $bridge = $this->getDatabase()->{$relation[1]};
232
233
                    $bridge
234
                        ->delete()
235
                        ->by($relation[2], $this->id)
236
                        ->run();
237
238
                    foreach ($this->relations[$name] as $row) {
239
                        $bridge
240
                            ->insert()
241
                            ->data([
242
                                $relation[2] => $this->id,
243
                                $relation[3] => $row->id,
244
                            ])
245
                            ->run();
246
                    }
247
                }
248
            }
249
        }
250
251
        $this->table->cache($this);
252
253
        return $this;
254
    }
255
}
256