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: 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: public function save()
227: {
228: $pre = $this->preSave();
229: if ($pre === false) {
230: return false;
231: }
232: $ret = $this->source()->saveItem($this);
233: if ($ret === false) {
234: return false;
235: }
236: $post = $this->postSave();
237: if ($post === false) {
238: return false;
239: }
240: return true;
241: }
242:
243: /**
244: * Update the object in storage to the current object state.
245: *
246: * @param array $properties If set, only update the properties specified in this array.
247: * @return boolean
248: */
249: public function update(array $properties = null)
250: {
251: $pre = $this->preUpdate($properties);
252: if ($pre === false) {
253: return false;
254: }
255: $ret = $this->source()->updateItem($this, $properties);
256: if ($ret === false) {
257: return false;
258: }
259: $post = $this->postUpdate($properties);
260: if ($post === false) {
261: return false;
262: }
263: return true;
264: }
265:
266: /**
267: * Delete an object from storage.
268: *
269: * @return boolean
270: */
271: public function delete()
272: {
273: $pre = $this->preDelete();
274: if ($pre === false) {
275: return false;
276: }
277: $ret = $this->source()->deleteItem($this);
278: $this->postDelete();
279: return $ret;
280: }
281:
282: /**
283: * @return boolean
284: */
285: protected function preSave()
286: {
287: return true;
288: }
289:
290: /**
291: * @return boolean
292: */
293: protected function postSave()
294: {
295: return true;
296: }
297:
298: /**
299: * @param string[] $keys Optional. The list of keys to update.
300: * @return boolean
301: */
302: protected function preUpdate(array $keys = null)
303: {
304: return true;
305: }
306:
307: /**
308: * @param string[] $keys Optional. The list of keys to update.
309: * @return boolean
310: */
311: protected function postUpdate(array $keys = null)
312: {
313: return true;
314: }
315:
316: /**
317: * @return boolean
318: */
319: protected function preDelete()
320: {
321: return true;
322: }
323:
324: /**
325: * @return boolean
326: */
327: protected function postDelete()
328: {
329: return true;
330: }
331: }
332: