1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie;
13
14 use ICanBoogie\ActiveRecord\Model;
15
16 /**
17 * Active Record faciliates the creation and use of business objects whose data require persistent
18 * storage via database.
19 *
20 * @property-read Model $model The model managing the active record.
21 * @property-read string $model_id The identifier of the model managing the active record.
22 */
23 class ActiveRecord extends \ICanBoogie\Object
24 {
25 /**
26 * Model managing the active record.
27 *
28 * @var Model
29 */
30 private $model;
31
32 /**
33 * Identifier of the model managing the active record.
34 *
35 * Note: Due to a PHP bug (or feature), the visibility of the property MUST NOT be private.
36 * https://bugs.php.net/bug.php?id=40412
37 *
38 * @var string
39 */
40 protected $model_id;
41
42 /**
43 * Initializes the {@link $model} and {@link $model_id} properties.
44 *
45 * @param string|Model $model The model managing the active record. A {@link Model}
46 * instance can be specified as well as a model identifier. If a model identifier is
47 * specified, the model is resolved when the {@link $model} property is accessed.
48 *
49 * @throws \InvalidArgumentException if $model is neither a model identifier nor a
50 * {@link Model} instance.
51 */
52 public function __construct($model)
53 {
54 if (is_string($model))
55 {
56 $this->model_id = $model;
57 }
58 else if ($model instanceof Model)
59 {
60 $this->model = $model;
61 $this->model_id = $model->id;
62 }
63 else
64 {
65 throw new \InvalidArgumentException("\$model must be an instance of ICanBoogie\ActiveRecord\Model or a model identifier. Given:" . (is_object($model) ? get_class($model) : gettype($model)));
66 }
67 }
68
69 /**
70 * Removes the {@link $model} property.
71 *
72 * Properties whose value are instances of the {@link ActiveRecord} class are removed from the
73 * exported properties.
74 */
75 public function __sleep()
76 {
77 $properties = parent::__sleep();
78
79 unset($properties['model']);
80
81 foreach ($properties as $property => $dummy)
82 {
83 if ($this->$property instanceof self)
84 {
85 unset($properties[$property]);
86 }
87 }
88
89 return $properties;
90 }
91
92 /**
93 * Returns the model managing the active record.
94 *
95 * This getter is used when the model has been provided as a string during construct.
96 *
97 * @return Model
98 */
99 protected function get_model()
100 {
101 if (!$this->model)
102 {
103 $this->model = ActiveRecord\get_model($this->model_id);
104 }
105
106 return $this->model;
107 }
108
109 /**
110 * Alias to {@link get_model}.
111 *
112 * @deprecated
113 *
114 * @see get_model
115 */
116 protected function get__model()
117 {
118 return $this->get_model();
119 }
120
121 /**
122 * Returns the identifier of the model managing the active record.
123 *
124 * The getter is used to provide read-only access to the property.
125 *
126 * @return string
127 */
128 protected function get_model_id()
129 {
130 return $this->model_id;
131 }
132
133 /**
134 * Alias to {@link get_model_id}.
135 *
136 * @deprecated
137 *
138 * @see get_model_id
139 */
140 protected function get__model_id()
141 {
142 return $this->get_model_id();
143 }
144
145 /**
146 * Saves the active record using its model.
147 *
148 * @return int Primary key value of the active record.
149 */
150 public function save()
151 {
152 $model = $this->get_model();
153 $schema = $model->extended_schema;
154
155 $properties = $this->to_array();
156 $properties = $this->alter_persistent_properties($properties, $model);
157
158 # removes the primary key from the properties.
159
160 $key = null;
161 $primary = $model->primary;
162
163 if (is_array($primary))
164 {
165 $rc = $model->insert($properties, [ 'on duplicate' => true ]);
166 }
167 else
168 {
169 $primary_definition = $primary ? $schema['fields'][$primary] : null;
170
171 if (isset($properties[$primary]) && empty($primary_definition['auto increment']))
172 {
173 $rc = $model->insert($properties, [ 'on duplicate' => true ]);
174 }
175 else
176 {
177 if (isset($properties[$primary]))
178 {
179 $key = $properties[$primary];
180 unset($properties[$primary]);
181 }
182
183 $rc = $model->save($properties, $key);
184
185 if ($key === null && $rc)
186 {
187 $this->$primary = $rc;
188 }
189 }
190 }
191
192 return $rc;
193 }
194
195 /**
196 * Unless it's an acceptable value for a column, columns with `null` values are discarted.
197 * This way, we don't have to define every properties before saving our active record.
198 *
199 * @param array $properties
200 *
201 * @return array The altered persistent properties
202 */
203 protected function alter_persistent_properties(array $properties, Model $model)
204 {
205 $schema = $model->extended_schema;
206
207 foreach ($properties as $identifier => $value)
208 {
209 if ($value !== null || (isset($schema['fields'][$identifier]) && !empty($schema['fields'][$identifier]['null'])))
210 {
211 continue;
212 }
213
214 unset($properties[$identifier]);
215 }
216
217 return $properties;
218 }
219
220 /**
221 * Deletes the active record using its model.
222 *
223 * @return bool `true` if the record was deleted, `false` otherwise.
224 */
225 public function delete()
226 {
227 $model = $this->model;
228 $primary = $model->primary;
229
230 return $model->delete($this->$primary);
231 }
232 }
233
234 namespace ICanBoogie\ActiveRecord;
235
236 /**
237 * Generic Active Record exception class.
238 */
239 class ActiveRecordException extends \Exception
240 {
241
242 }