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