ensure accessing a cell on ref update
[m6w6/pq-gateway] / lib / pq / Gateway / Row.php
index 885c95abd5a166b51cb35e6281aa72280e6a67f7..cda8d4af67a0ba857a6854b4f8c262f016f5e34d 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace pq\Gateway;
 
+use \pq\Query\Expr as QueryExpr;
+
 class Row implements \JsonSerializable
 {
        /**
@@ -17,7 +19,7 @@ class Row implements \JsonSerializable
        /**
         * @var array
         */
-       protected $mods = array();
+       protected $cell = array();
        
        /**
         * @param \pq\Gateway\Table $table
@@ -26,7 +28,7 @@ class Row implements \JsonSerializable
         */
        function __construct(Table $table, array $data = null, $prime = false) {
                $this->table = $table;
-               $this->data = $data;
+               $this->data = (array) $data;
                
                if ($prime) {
                        $this->prime();
@@ -43,13 +45,40 @@ class Row implements \JsonSerializable
                $that->data = $data;
                return $that->prime();
        }
+
+       /**
+        * Export current state as an array
+        * @return array
+        * @throws \UnexpectedValueException if a cell has been modified by an expression
+        */
+       function export() {
+               $export = array_merge($this->data, $this->cell);
+               foreach ($export as &$val) {
+                       if ($val instanceof Cell) {
+                               if ($val->isExpr()) {
+                                       throw new \UnexpectedValueException("Cannot export an SQL expression");
+                               }
+                               $val = $val->get();
+                       }
+               }
+               return $export;
+       }
        
+       /**
+        * Export current state with security sensitive data removed. You should override that.
+        * Just calls export() by default.
+        * @return array
+        */
+       function exportPublic() {
+               return $this->export();
+       }
+
        /**
         * @implements JsonSerializable
         * @return array
         */
        function jsonSerialize() {
-               return $this->data;
+               return $this->exportPublic();
        }
        
        /**
@@ -66,41 +95,117 @@ class Row implements \JsonSerializable
                return $this->data;
        }
        
+       /**
+        * Get all column/value pairs to possibly uniquely identify this row
+        * @return array
+        * @throws \OutOfBoundsException if any primary key column is not present in the row
+        */
+       function getIdentity() {
+               $cols = array();
+               if (count($identity = $this->getTable()->getIdentity())) {
+                       foreach ($identity as $col) {
+                               if (!array_key_exists($col, $this->data)) {
+                                       throw new \OutOfBoundsException(
+                                               sprintf("Column '%s' does not exist in row of table '%s'",
+                                                       $col, $this->getTable()->getName()
+                                               )
+                                       );
+                               }
+                               $cols[$col] = $this->data[$col];
+                       }
+               } else {
+                       $cols = $this->data;
+               }
+               return $cols;
+       }
+       
+       /**
+        * Check whether the row contains modifications
+        * @return boolean
+        */
+       function isDirty() {
+               foreach ($this->cell as $cell) {
+                       if ($cell->isDirty()) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+       
+       /**
+        * Refresh the rows data
+        * @return \pq\Gateway\Row
+        */
+       function refresh() {
+               $this->data = $this->table->find($this->criteria(), null, 1, 0)->current()->data;
+               $this->cell = array();
+               return $this;
+       }
+       
        /**
         * Fill modified cells
         * @return \pq\Gateway\Row
         */
        protected function prime() {
-               $this->mods = array();
+               $this->cell = array();
                foreach ($this->data as $key => $val) {
-                       $this->mods[$key] = new Cell($this, $key, $val);
+                       $this->cell[$key] = new Cell($this, $key, $val, true);
                }
                return $this;
        }
        
        /**
-        * Transform data array to where criteria
-        * @param array $data
+        * Transform the row's identity to where criteria
         * @return array
         */
        protected function criteria() {
                $where = array();
-               array_walk($this->data, function($v, $k) use (&$where) {
-                       $where["$k="] = $v;
-               });
+               foreach ($this->getIdentity() as $col => $val) {
+                       if (isset($val)) {
+                               $where["$col="] = $val;
+                       } else {
+                               $where["$col IS"] = new QueryExpr("NULL");
+                       }
+               }
                return $where;
        }
        
        /**
-        * Get a cell
-        * @param string $p
+        * Get an array of changed properties
+        * @return array
+        */
+       protected function changes() {
+               $changes = array();
+               foreach ($this->cell as $name => $cell) {
+                       if ($cell->isDirty()) {
+                               $changes[$name] = $cell->get();
+                       }
+               }
+               return $changes;
+       }
+       
+       /**
+        * Cell accessor
+        * @param string $p column name
         * @return \pq\Gateway\Cell
         */
+       protected function cell($p) {
+               if (!isset($this->cell[$p])) {
+                       $this->cell[$p] = new Cell($this, $p, isset($this->data[$p]) ? $this->data[$p] : null);
+               }
+               return $this->cell[$p];
+       }
+       
+       /**
+        * Get a cell or parent rows
+        * @param string $p
+        * @return \pq\Gateway\Cell|\pq\Gateway\Rowset
+        */
        function __get($p) {
-               if (!isset($this->mods[$p])) {
-                       $this->mods[$p] = new Cell($this, $p, $this->data[$p]);
+               if ($this->table->hasRelation($p)) {
+                       return $this->table->by($this, $p);
                }
-               return $this->mods[$p];
+               return $this->cell($p);
        }
        
        /**
@@ -109,7 +214,38 @@ class Row implements \JsonSerializable
         * @param mixed $v
         */
        function __set($p, $v) {
-               $this->__get($p)->set(($v instanceof Cell) ? $v->get() : $v);
+               $this->cell($p)->set($v);
+       }
+       
+       /**
+        * Unset a cell value
+        * @param string $p
+        */
+       function __unset($p) {
+               unset($this->data[$p]);
+               unset($this->cell[$p]);
+       }
+       
+       /**
+        * Check if a cell isset
+        * @param string $p
+        * @return bool
+        */
+       function __isset($p) {
+               return isset($this->data[$p]) || isset($this->cell[$p]);
+       }
+       
+       /**
+        * Get child rows of this row by foreign key
+        * @see \pq\Gateway\Table::of()
+        * @param string $foreign
+        * @param array $args [order, limit, offset]
+        * @return \pq\Gateway\Rowset
+        */
+       function __call($foreign, array $args) {
+               array_unshift($args, $this);
+               $table = forward_static_call(array(get_class($this->getTable()), "resolve"), $foreign);
+               return call_user_func_array(array($table, "of"), $args);
        }
        
        /**
@@ -117,8 +253,12 @@ class Row implements \JsonSerializable
         * @return \pq\Gateway\Row
         */
        function create() {
-               $this->data = $this->table->create($this->mods)->current()->data;
-               $this->mods = array();
+               $rowset = $this->table->create($this->changes());
+               if (!count($rowset)) {
+                       throw new \UnexpectedValueException("No row created");
+               }
+               $this->data = $rowset->current()->data;
+               $this->cell = array();
                return $this;
        }
        
@@ -127,8 +267,16 @@ class Row implements \JsonSerializable
         * @return \pq\Gateway\Row
         */
        function update() {
-               $this->data = $this->table->update($this->criteria(), $this->mods)->current()->data;
-               $this->mods = array();
+               $criteria = $this->criteria();
+               if (($lock = $this->getTable()->getLock())) {
+                       $lock->onUpdate($this, $criteria);
+               }
+               $rowset = $this->table->update($criteria, $this->changes());
+               if (!count($rowset)) {
+                       throw new \UnexpectedValueException("No row updated");
+               }
+               $this->data = $rowset->current()->data;
+               $this->cell = array();
                return $this;
        }
        
@@ -137,7 +285,11 @@ class Row implements \JsonSerializable
         * @return \pq\Gateway\Row
         */
        function delete() {
-               $this->data = $this->table->delete($this->criteria(), "*")->current()->data;
+               $rowset = $this->table->delete($this->criteria(), "*");
+               if (!count($rowset)) {
+                       throw new \UnexpectedValueException("No row deleted");
+               }
+               $this->data = $rowset->current()->data;
                return $this->prime();
        }
 }