ensure accessing a cell on ref update
[m6w6/pq-gateway] / lib / pq / Gateway / Row.php
1 <?php
2
3 namespace pq\Gateway;
4
5 use \pq\Query\Expr as QueryExpr;
6
7 class Row implements \JsonSerializable
8 {
9 /**
10 * @var \pq\Gateway\Table
11 */
12 protected $table;
13
14 /**
15 * @var array
16 */
17 protected $data;
18
19 /**
20 * @var array
21 */
22 protected $cell = array();
23
24 /**
25 * @param \pq\Gateway\Table $table
26 * @param array $data
27 * @param bool $prime whether to mark all columns as modified
28 */
29 function __construct(Table $table, array $data = null, $prime = false) {
30 $this->table = $table;
31 $this->data = (array) $data;
32
33 if ($prime) {
34 $this->prime();
35 }
36 }
37
38 /**
39 * Copy constructor
40 * @param array $data
41 * @return \pq\Gateway\Row
42 */
43 function __invoke(array $data) {
44 $that = clone $this;
45 $that->data = $data;
46 return $that->prime();
47 }
48
49 /**
50 * Export current state as an array
51 * @return array
52 * @throws \UnexpectedValueException if a cell has been modified by an expression
53 */
54 function export() {
55 $export = array_merge($this->data, $this->cell);
56 foreach ($export as &$val) {
57 if ($val instanceof Cell) {
58 if ($val->isExpr()) {
59 throw new \UnexpectedValueException("Cannot export an SQL expression");
60 }
61 $val = $val->get();
62 }
63 }
64 return $export;
65 }
66
67 /**
68 * Export current state with security sensitive data removed. You should override that.
69 * Just calls export() by default.
70 * @return array
71 */
72 function exportPublic() {
73 return $this->export();
74 }
75
76 /**
77 * @implements JsonSerializable
78 * @return array
79 */
80 function jsonSerialize() {
81 return $this->exportPublic();
82 }
83
84 /**
85 * @return \pq\Gateway\Table
86 */
87 function getTable() {
88 return $this->table;
89 }
90
91 /**
92 * @return array
93 */
94 function getData() {
95 return $this->data;
96 }
97
98 /**
99 * Get all column/value pairs to possibly uniquely identify this row
100 * @return array
101 * @throws \OutOfBoundsException if any primary key column is not present in the row
102 */
103 function getIdentity() {
104 $cols = array();
105 if (count($identity = $this->getTable()->getIdentity())) {
106 foreach ($identity as $col) {
107 if (!array_key_exists($col, $this->data)) {
108 throw new \OutOfBoundsException(
109 sprintf("Column '%s' does not exist in row of table '%s'",
110 $col, $this->getTable()->getName()
111 )
112 );
113 }
114 $cols[$col] = $this->data[$col];
115 }
116 } else {
117 $cols = $this->data;
118 }
119 return $cols;
120 }
121
122 /**
123 * Check whether the row contains modifications
124 * @return boolean
125 */
126 function isDirty() {
127 foreach ($this->cell as $cell) {
128 if ($cell->isDirty()) {
129 return true;
130 }
131 }
132 return false;
133 }
134
135 /**
136 * Refresh the rows data
137 * @return \pq\Gateway\Row
138 */
139 function refresh() {
140 $this->data = $this->table->find($this->criteria(), null, 1, 0)->current()->data;
141 $this->cell = array();
142 return $this;
143 }
144
145 /**
146 * Fill modified cells
147 * @return \pq\Gateway\Row
148 */
149 protected function prime() {
150 $this->cell = array();
151 foreach ($this->data as $key => $val) {
152 $this->cell[$key] = new Cell($this, $key, $val, true);
153 }
154 return $this;
155 }
156
157 /**
158 * Transform the row's identity to where criteria
159 * @return array
160 */
161 protected function criteria() {
162 $where = array();
163 foreach ($this->getIdentity() as $col => $val) {
164 if (isset($val)) {
165 $where["$col="] = $val;
166 } else {
167 $where["$col IS"] = new QueryExpr("NULL");
168 }
169 }
170 return $where;
171 }
172
173 /**
174 * Get an array of changed properties
175 * @return array
176 */
177 protected function changes() {
178 $changes = array();
179 foreach ($this->cell as $name => $cell) {
180 if ($cell->isDirty()) {
181 $changes[$name] = $cell->get();
182 }
183 }
184 return $changes;
185 }
186
187 /**
188 * Cell accessor
189 * @param string $p column name
190 * @return \pq\Gateway\Cell
191 */
192 protected function cell($p) {
193 if (!isset($this->cell[$p])) {
194 $this->cell[$p] = new Cell($this, $p, isset($this->data[$p]) ? $this->data[$p] : null);
195 }
196 return $this->cell[$p];
197 }
198
199 /**
200 * Get a cell or parent rows
201 * @param string $p
202 * @return \pq\Gateway\Cell|\pq\Gateway\Rowset
203 */
204 function __get($p) {
205 if ($this->table->hasRelation($p)) {
206 return $this->table->by($this, $p);
207 }
208 return $this->cell($p);
209 }
210
211 /**
212 * Set a cell value
213 * @param string $p
214 * @param mixed $v
215 */
216 function __set($p, $v) {
217 $this->cell($p)->set($v);
218 }
219
220 /**
221 * Unset a cell value
222 * @param string $p
223 */
224 function __unset($p) {
225 unset($this->data[$p]);
226 unset($this->cell[$p]);
227 }
228
229 /**
230 * Check if a cell isset
231 * @param string $p
232 * @return bool
233 */
234 function __isset($p) {
235 return isset($this->data[$p]) || isset($this->cell[$p]);
236 }
237
238 /**
239 * Get child rows of this row by foreign key
240 * @see \pq\Gateway\Table::of()
241 * @param string $foreign
242 * @param array $args [order, limit, offset]
243 * @return \pq\Gateway\Rowset
244 */
245 function __call($foreign, array $args) {
246 array_unshift($args, $this);
247 $table = forward_static_call(array(get_class($this->getTable()), "resolve"), $foreign);
248 return call_user_func_array(array($table, "of"), $args);
249 }
250
251 /**
252 * Create this row in the database
253 * @return \pq\Gateway\Row
254 */
255 function create() {
256 $rowset = $this->table->create($this->changes());
257 if (!count($rowset)) {
258 throw new \UnexpectedValueException("No row created");
259 }
260 $this->data = $rowset->current()->data;
261 $this->cell = array();
262 return $this;
263 }
264
265 /**
266 * Update this row in the database
267 * @return \pq\Gateway\Row
268 */
269 function update() {
270 $criteria = $this->criteria();
271 if (($lock = $this->getTable()->getLock())) {
272 $lock->onUpdate($this, $criteria);
273 }
274 $rowset = $this->table->update($criteria, $this->changes());
275 if (!count($rowset)) {
276 throw new \UnexpectedValueException("No row updated");
277 }
278 $this->data = $rowset->current()->data;
279 $this->cell = array();
280 return $this;
281 }
282
283 /**
284 * Delete this row in the database
285 * @return \pq\Gateway\Row
286 */
287 function delete() {
288 $rowset = $this->table->delete($this->criteria(), "*");
289 if (!count($rowset)) {
290 throw new \UnexpectedValueException("No row deleted");
291 }
292 $this->data = $rowset->current()->data;
293 return $this->prime();
294 }
295 }