1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Charcoal\Source; |
4
|
|
|
|
5
|
|
|
use \Exception; |
6
|
|
|
use \InvalidArgumentException; |
7
|
|
|
|
8
|
|
|
// Module `charcoal-factory` dependencies |
9
|
|
|
use \Charcoal\Factory\FactoryInterface; |
10
|
|
|
|
11
|
|
|
// Local namespace dependencies |
12
|
|
|
use \Charcoal\Source\SourceInterface; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Full implementation, as trait, of the StorableInterface |
16
|
|
|
*/ |
17
|
|
|
trait StorableTrait |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* @var mixed $id The object (unique) identifier |
21
|
|
|
*/ |
22
|
|
|
protected $id; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var string $key The object key |
26
|
|
|
*/ |
27
|
|
|
protected $key = 'id'; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var FactoryInterface $sourceFactory |
31
|
|
|
*/ |
32
|
|
|
protected $sourceFactory; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var SourceInterface $source |
36
|
|
|
*/ |
37
|
|
|
private $source; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Set the object's ID. The actual property set depends on `key()` |
41
|
|
|
* |
42
|
|
|
* @param mixed $id The object id (identifier / primary key value). |
43
|
|
|
* @throws InvalidArgumentException If the argument is not scalar. |
44
|
|
|
* @return StorableInterface Chainable |
45
|
|
|
*/ |
46
|
|
|
public function setId($id) |
47
|
|
|
{ |
48
|
|
|
if (!is_scalar($id)) { |
49
|
|
|
throw new InvalidArgumentException( |
50
|
|
|
sprintf( |
51
|
|
|
'ID for "%s" must be a scalar (integer, float, string, or boolean); received %s', |
52
|
|
|
get_class($this), |
53
|
|
|
(is_object($id) ? get_class($id) : gettype($id)) |
54
|
|
|
) |
55
|
|
|
); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
$key = $this->key(); |
59
|
|
|
if ($key == 'id') { |
60
|
|
|
$this->id = $id; |
61
|
|
|
} else { |
62
|
|
|
$this[$key] = $id; |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
return $this; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Get the object's (unique) ID. The actualy property get depends on `key()` |
70
|
|
|
* |
71
|
|
|
* @throws Exception If the set key is invalid. |
72
|
|
|
* @return mixed |
73
|
|
|
*/ |
74
|
|
|
public function id() |
75
|
|
|
{ |
76
|
|
|
$key = $this->key(); |
77
|
|
|
if ($key == 'id') { |
78
|
|
|
return $this->id; |
79
|
|
|
} else { |
80
|
|
|
return $this[$key]; |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Set the key property. |
86
|
|
|
* |
87
|
|
|
* @param string $key The object key, or identifier "name". |
88
|
|
|
* @throws InvalidArgumentException If the argument is not scalar. |
89
|
|
|
* @return StorableInterface Chainable |
90
|
|
|
*/ |
91
|
|
View Code Duplication |
public function setKey($key) |
|
|
|
|
92
|
|
|
{ |
93
|
|
|
if (!is_string($key)) { |
94
|
|
|
throw new InvalidArgumentException( |
95
|
|
|
sprintf( |
96
|
|
|
'Key must be a string; received %s', |
97
|
|
|
(is_object($key) ? get_class($key) : gettype($key)) |
98
|
|
|
) |
99
|
|
|
); |
100
|
|
|
} |
101
|
|
|
// For security reason, only alphanumeric characters (+ underscores) are valid key names. |
102
|
|
|
// Although SQL can support more, there's really no reason to. |
103
|
|
|
if (!preg_match_all('/^[A-Za-z0-9_]+$/', $key)) { |
104
|
|
|
throw new InvalidArgumentException( |
105
|
|
|
sprintf('Key "%s" is invalid: must be alphanumeric / underscore.', $key) |
106
|
|
|
); |
107
|
|
|
} |
108
|
|
|
$this->key = $key; |
109
|
|
|
|
110
|
|
|
return $this; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Get the key property. |
115
|
|
|
* |
116
|
|
|
* @return string |
117
|
|
|
*/ |
118
|
|
|
public function key() |
119
|
|
|
{ |
120
|
|
|
return $this->key; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @param FactoryInterface $factory The source factory, which is useful to create source. |
125
|
|
|
* @return StorableInterface Chainable |
126
|
|
|
*/ |
127
|
|
|
protected function setSourceFactory(FactoryInterface $factory) |
128
|
|
|
{ |
129
|
|
|
$this->sourceFactory = $factory; |
130
|
|
|
return $this; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @throws Exception If the source factory was not previously set. |
135
|
|
|
* @return FactoryInterface |
136
|
|
|
*/ |
137
|
|
|
protected function sourceFactory() |
138
|
|
|
{ |
139
|
|
|
if (!isset($this->sourceFactory)) { |
140
|
|
|
throw new Exception( |
141
|
|
|
sprintf('Source factory is not set for "%s"', get_class($this)) |
142
|
|
|
); |
143
|
|
|
} |
144
|
|
|
return $this->sourceFactory; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Set the object's source. |
149
|
|
|
* |
150
|
|
|
* @param SourceInterface $source The storable object's source. |
151
|
|
|
* @return StorableInterface Chainable |
152
|
|
|
* @todo This method needs to go protected. |
153
|
|
|
*/ |
154
|
|
|
public function setSource(SourceInterface $source) |
155
|
|
|
{ |
156
|
|
|
$this->source = $source; |
157
|
|
|
return $this; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Get the object's source. |
162
|
|
|
* |
163
|
|
|
* @return SourceInterface |
164
|
|
|
*/ |
165
|
|
|
public function source() |
166
|
|
|
{ |
167
|
|
|
if ($this->source === null) { |
168
|
|
|
$this->source = $this->createSource(); |
169
|
|
|
} |
170
|
|
|
return $this->source; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Create the model's source, with the Source Factory. |
175
|
|
|
* |
176
|
|
|
* @return SourceInterface |
177
|
|
|
*/ |
178
|
|
|
abstract protected function createSource(); |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Load an object from the database from its ID. |
182
|
|
|
* |
183
|
|
|
* @param mixed $id The identifier to load. |
184
|
|
|
* @return StorableInterface Chainable |
185
|
|
|
*/ |
186
|
|
|
public function load($id = null) |
187
|
|
|
{ |
188
|
|
|
if ($id === null) { |
189
|
|
|
$id = $this->id(); |
190
|
|
|
} |
191
|
|
|
$this->source()->loadItem($id, $this); |
192
|
|
|
return $this; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Load an object from the database from its key $key. |
197
|
|
|
* |
198
|
|
|
* @param string $key Key pointing a column's name. |
199
|
|
|
* @param mixed $value Value of said column. |
200
|
|
|
* @return StorableInterface Chainable. |
201
|
|
|
*/ |
202
|
|
|
public function loadFrom($key = null, $value = null) |
203
|
|
|
{ |
204
|
|
|
$this->source()->loadItemFromKey($key, $value, $this); |
|
|
|
|
205
|
|
|
return $this; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Load an object from the database from a custom SQL query. |
210
|
|
|
* |
211
|
|
|
* @param string $query The SQL query. |
212
|
|
|
* @param array $binds Optional. The SQL query parameters. |
213
|
|
|
* @return StorableInterface Chainable. |
214
|
|
|
*/ |
215
|
|
|
public function loadFromQuery($query, array $binds = []) |
216
|
|
|
{ |
217
|
|
|
$this->source()->loadItemFromQuery($query, $binds, $this); |
|
|
|
|
218
|
|
|
return $this; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Save an object current state to storage |
223
|
|
|
* |
224
|
|
|
* @return boolean |
225
|
|
|
*/ |
226
|
|
View Code Duplication |
public function save() |
|
|
|
|
227
|
|
|
{ |
228
|
|
|
$pre = $this->preSave(); |
229
|
|
|
if ($pre === false) { |
230
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
231
|
|
|
'Can not save object (%s:%s). The preSave() method failed.', |
232
|
|
|
$this->objType(), |
|
|
|
|
233
|
|
|
$this->id() |
234
|
|
|
)); |
235
|
|
|
return false; |
236
|
|
|
} |
237
|
|
|
$ret = $this->source()->saveItem($this); |
238
|
|
|
if ($ret === false) { |
239
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
240
|
|
|
'Can not save object (%s:%s). The source\'s saveItem() method failed.', |
241
|
|
|
$this->objType(), |
|
|
|
|
242
|
|
|
$this->id() |
243
|
|
|
)); |
244
|
|
|
return false; |
245
|
|
|
} |
246
|
|
|
$post = $this->postSave(); |
247
|
|
|
if ($post === false) { |
248
|
|
|
$this->logger()->warning(sprintf( |
|
|
|
|
249
|
|
|
'Although the object (%s:%s) was saved, the postSave() method had an error. Expect problems later.', |
250
|
|
|
$this->objType(), |
|
|
|
|
251
|
|
|
$this->id() |
252
|
|
|
)); |
253
|
|
|
return false; |
254
|
|
|
} |
255
|
|
|
return true; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Update the object in storage to the current object state. |
260
|
|
|
* |
261
|
|
|
* @param array $properties If set, only update the properties specified in this array. |
262
|
|
|
* @return boolean |
263
|
|
|
*/ |
264
|
|
|
public function update(array $properties = null) |
265
|
|
|
{ |
266
|
|
|
$pre = $this->preUpdate($properties); |
|
|
|
|
267
|
|
|
if ($pre === false) { |
268
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
269
|
|
|
'Can not update object (%s:%s). The preUpdate() method failed.', |
270
|
|
|
$this->objType(), |
|
|
|
|
271
|
|
|
$this->id() |
272
|
|
|
)); |
273
|
|
|
return false; |
274
|
|
|
} |
275
|
|
|
$ret = $this->source()->updateItem($this, $properties); |
276
|
|
|
if ($ret === false) { |
277
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
278
|
|
|
'Can not update object (%s:%s). The source\'s updateItem() method failed.', |
279
|
|
|
$this->objType(), |
|
|
|
|
280
|
|
|
$this->id() |
281
|
|
|
)); |
282
|
|
|
return false; |
283
|
|
|
} |
284
|
|
|
$post = $this->postUpdate($properties); |
|
|
|
|
285
|
|
|
if ($post === false) { |
286
|
|
|
$this->logger()->warning(sprintf( |
|
|
|
|
287
|
|
|
'Although the object (%s:%s) was updated, the postUpdate() method had an error. Expect problems later.', |
288
|
|
|
$this->objType(), |
|
|
|
|
289
|
|
|
$this->id() |
290
|
|
|
)); |
291
|
|
|
return false; |
292
|
|
|
} |
293
|
|
|
return true; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Delete an object from storage. |
298
|
|
|
* |
299
|
|
|
* @return boolean |
300
|
|
|
*/ |
301
|
|
View Code Duplication |
public function delete() |
|
|
|
|
302
|
|
|
{ |
303
|
|
|
$pre = $this->preDelete(); |
304
|
|
|
if ($pre === false) { |
305
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
306
|
|
|
'Can not update object (%s:%s). The preUpdate() method failed.', |
307
|
|
|
$this->objType(), |
|
|
|
|
308
|
|
|
$this->id() |
309
|
|
|
)); |
310
|
|
|
return false; |
311
|
|
|
} |
312
|
|
|
$ret = $this->source()->deleteItem($this); |
313
|
|
|
if ($ret === false) { |
314
|
|
|
$this->logger()->error(sprintf( |
|
|
|
|
315
|
|
|
'Can not delete object (%s:%s). The source\'s deleteItem() method failed.', |
316
|
|
|
$this->objType(), |
|
|
|
|
317
|
|
|
$this->id() |
318
|
|
|
)); |
319
|
|
|
return false; |
320
|
|
|
} |
321
|
|
|
$del = $this->postDelete(); |
322
|
|
|
if ($del === false) { |
323
|
|
|
$this->logger()->warning(sprintf( |
|
|
|
|
324
|
|
|
'Although the object (%s:%s) was deleted, the postDelete() method had an error. Expect problems later.', |
325
|
|
|
$this->objType(), |
|
|
|
|
326
|
|
|
$this->id() |
327
|
|
|
)); |
328
|
|
|
return false; |
329
|
|
|
} |
330
|
|
|
return true; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* @return boolean |
335
|
|
|
*/ |
336
|
|
|
protected function preSave() |
337
|
|
|
{ |
338
|
|
|
return true; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* @return boolean |
343
|
|
|
*/ |
344
|
|
|
protected function postSave() |
345
|
|
|
{ |
346
|
|
|
return true; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* @param string[] $keys Optional. The list of keys to update. |
351
|
|
|
* @return boolean |
352
|
|
|
*/ |
353
|
|
|
protected function preUpdate(array $keys = null) |
|
|
|
|
354
|
|
|
{ |
355
|
|
|
return true; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* @param string[] $keys Optional. The list of keys to update. |
360
|
|
|
* @return boolean |
361
|
|
|
*/ |
362
|
|
|
protected function postUpdate(array $keys = null) |
|
|
|
|
363
|
|
|
{ |
364
|
|
|
return true; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* @return boolean |
369
|
|
|
*/ |
370
|
|
|
protected function preDelete() |
371
|
|
|
{ |
372
|
|
|
return true; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* @return boolean |
377
|
|
|
*/ |
378
|
|
|
protected function postDelete() |
379
|
|
|
{ |
380
|
|
|
return true; |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
|
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.