Add data\Model from phalanx
authorRobert Sesek <rsesek@google.com>
Sat, 30 Jul 2011 16:33:26 +0000 (12:33 -0400)
committerRobert Sesek <rsesek@google.com>
Sat, 30 Jul 2011 16:33:26 +0000 (12:33 -0400)
data/model.php [new file with mode: 0644]
testing/tests/data/model_test.php [new file with mode: 0644]

diff --git a/data/model.php b/data/model.php
new file mode 100644 (file)
index 0000000..cf5979f
--- /dev/null
@@ -0,0 +1,196 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\data;
+
+require_once HOPLITE_ROOT . '/base/struct.php';
+
+/*!
+  A Model represents a single instance of a database row and its relations.
+  It inherits the strict data member policy from base\Struct and maintains
+  a condition by which it fetches and updates data. The role of the Model
+  class is not to validate data, but to persist it. Validation is the job
+  of the Controller (in phalanx, that is a Task object); the Model will
+  persist the data and help access related information.
+  
+  This class requires the use of a PDO object.
+*/
+class Model extends \hoplite\base\Struct
+{
+  /*! @var PDO The object the model will use when performing operations. */
+  static protected $db = NULL;
+
+  /*! @var string The string prefix to put in front of the table name. */
+  protected $table_prefix = '';
+
+  /*! @var string The name of the database table to which the object belongs. */
+  protected $table = 'table';
+
+  /*! @var string The condition to select this data object by. Parameters should
+                  be keyed using :keyname syntax.
+  */
+  protected $condition = 'pkey = :pkey';
+
+  /*! @var string The name of the field(s) that provide the primary key. This
+                  can either be a single string or an array for a compound key.
+
+    A word on primary keys: This class assumes that if the primary key is
+    singular (defined as a string), then it is auto-increment. The
+    implications of this are that the set primary key value is igorned on
+    inserts and upates. If you do not want this behavior for a table with a
+    truly singluar primary key, define it as a plural/compound primary key
+    in the Model (defined as an array).
+  */
+  protected $primary_key = 'pkey';
+
+  public function condition() { return $this->condition; }
+  public function set_condition($cond) { $this->condition = $cond; }
+
+  static public function db() { return self::$db; }
+  static public function set_db(\PDO $db) { self::$db = $db; }
+
+  /*!
+    Constructor. This takes in either the value(s) to substitute into the
+    |$this->condition| or NULL to create a new instance of the model.
+  */
+  public function __construct($condition_data = NULL)
+  {
+    if (is_array($condition_data)) {
+      if (!is_array($this->primary_key))
+        throw new ModelException('Cannot create ' . get_class($this) . ' with an array when primary key is singular.');
+
+      foreach ($condition_data as $key => $value)
+        if (in_array($key, $this->primary_key))
+          $this->Set($key, $value);
+    } else if (!is_null($condition_data)) {
+      if (is_array($this->primary_key))
+        throw new ModelException('Cannot create ' . get_class($this) . ' when a singular value is given for a compound primary key.');
+      $this->Set($this->primary_key, $condition_data);
+    }
+
+    $this->table = $this->table_prefix . $this->table;
+  }
+
+  /*! Fetches an object and returns the result based on the |$this->condition|. */
+  public function Fetch()
+  {
+    $stmt = self::$db->Prepare("SELECT * FROM {$this->table} WHERE " . $this->condition());
+    $stmt->Execute($this->_GetSQLParams($stmt));
+    $result = $stmt->FetchObject();
+    if (!$result)
+      throw new ModelException("Could not fetch " . get_class($this));
+    return $result;
+  }
+
+  /*!
+    Fetches an object and stores the result in the model, overwriting existing
+    data values.
+  */
+  public function FetchInto()
+  {
+    $this->SetFrom($this->Fetch());
+  }
+
+  /*!
+    Inserts the new model into the database. This will explicitly filter out
+    primary key information if it is a singular key.
+  */
+  public function Insert()
+  {
+    $data = $this->ToArray();
+    if (!is_array($this->primary_key))
+      unset($data[$this->primary_key]);
+
+    $keys = array_keys($data);
+    $placeholders = array_map(function ($s) { return ":$s"; }, $keys);
+    $stmt = self::$db->Prepare("
+      INSERT INTO {$this->table}
+        (" . implode(', ', $keys) . ")
+      VALUES
+        (" . implode(', ', $placeholders) . ")
+    ");
+    $stmt->Execute($data);
+
+    if (!is_array($this->primary_key))
+      $this->Set($this->primary_key, self::$db->LastInsertID());
+  }
+
+  /*! Updates the database based on the values set in the model. */
+  public function Update()
+  {
+    $updates   = array_map(function($s) { return "$s = :$s"; }, array_keys($this->ToArray()));
+    $condition = $this->condition();
+    $stmt = self::$db->Prepare("UPDATE {$this->table} SET " . implode(', ', $updates) . " WHERE $condition");
+    $stmt->Execute($this->_GetSQLParams($stmt));
+  }
+
+  /*! Deletes a record in the database based on the set condition. */
+  public function Delete()
+  {
+    $stmt = self::$db->Prepare("DELETE FROM {$this->table} WHERE " . $this->condition());
+    $stmt->Execute($this->_GetSQLParams($stmt));
+  }
+
+  /*!
+    Returns a subest of |$this->data| that is required to execute a given
+    PDOStatement. This will only work on queries whose parameters are
+    specified using :name syntax. It will filter all values that are not
+    used in the query.
+  */
+  protected function _GetSQLParams(\PDOStatement $stmt)
+  {
+    $matches = array();
+    preg_match_all('/\:([a-z0-9_\-]+)/i', $stmt->queryString, $matches);
+    $params = array();
+    $data   = $this->ToArray();
+
+    foreach ($matches[1] as $key)
+      $params[$key] = $data[$key];      
+
+    return $params;
+  }
+
+  /*!
+    Returns an array of Model objects of the correct type based on a group
+    condition. If no condition is specified, returns all results in the table.
+    If the |$params| argument is not an array, it will be assumed to be a
+    single value and will be converted to an array.
+  */
+  static public function FetchGroup($condition = '', $params = array())
+  {
+    $class = get_called_class();  // Late static binding.
+    $props = new $class();
+    $sql = "SELECT * FROM {$props->table_prefix}{$props->table}";
+    if ($condition)
+      $sql .= " WHERE $condition";
+    $stmt = self::$db->Prepare($sql);
+
+    if (!is_array($params))
+      $params = array($params);
+    $stmt->Execute($params);
+
+    $results = array();
+    while ($obj = $stmt->FetchObject()) {
+      $model = new $class();
+      $model->SetFrom($obj);
+      $results[] = $model;
+    }
+
+    return $results;
+  }
+}
+
+class ModelException extends \Exception {}
diff --git a/testing/tests/data/model_test.php b/testing/tests/data/model_test.php
new file mode 100644 (file)
index 0000000..b223e35
--- /dev/null
@@ -0,0 +1,210 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+namespace hoplite\test;
+use \hoplite\data as data;
+
+class CompoundKeyModel extends data\Model
+{
+  protected $table = 'test_compound';
+  protected $primary_key = array('id_1', 'id_2');
+  protected $condition = 'id_1 = :id_1 AND id_2 = :id_2';
+
+  protected $fields = array(
+    'id_1',
+    'id_2',
+    'value'
+  );
+}
+
+class PrefixTest extends data\Model
+{
+  protected $table_prefix = 'test_';
+  protected $table = 'prefix';
+
+  public function table() { return $this->table; }
+}
+
+class ModelTest extends \PHPUnit_Framework_TestCase
+{
+  public $db;
+
+  public function setUp()
+  {
+    $this->db = TestModel::SetUpDatabase();
+    $this->db->Query("
+      CREATE TABLE test_compound
+      (
+        id_1 integer,
+        id_2 integer,
+        value text,
+        PRIMARY KEY (id_1, id_2)
+      );
+    ");
+    TestModel::set_db($this->db);
+    $this->assertSame($this->db, TestModel::db());
+    CompoundKeyModel::set_db($this->db);
+    $this->assertSame($this->db, CompoundKeyModel::db());
+  }
+
+  public function testBadCreate()
+  {
+    $this->setExpectedException('hoplite\data\ModelException');
+    $model = new TestModel(array('id' => 1));
+  }
+
+  public function testInsert()
+  {
+    $model = new TestModel();
+    $model->title = 'Hello';
+    $model->description = 'A test';
+    $model->Insert();
+    $this->assertEquals(1, $model->id);
+    $model->Insert();
+    $this->assertEquals(2, $model->id);
+  }
+
+  public function testFetch()
+  {
+    $this->testInsert();
+    $model = new TestModel(1);
+    $model->FetchInto();
+    $this->assertEquals('Hello', $model->title);
+    $this->assertEquals('A test', $model->description);
+  }
+
+  public function testFetchCustomCondition()
+  {
+    $model = new TestModel();
+    $model->title = 'test';
+    $model->description = 'foobar';
+    $model->Insert();
+
+    $model = new TestModel();
+    $model->set_condition('title = :title');
+    $model->title = 'test';
+    $model->FetchInto();
+    $this->assertEquals('foobar', $model->description);
+  }
+
+  public function testFetchGroup()
+  {
+    $model = new TestModel();
+    $model->title = 'test';
+    $model->description = 'foo';
+    $model->Insert();
+
+    $model = new TestModel();
+    $model->title = 'test';
+    $model->description = 'bar';
+    $model->Insert();
+
+    $model = new TestModel();
+    $model->title = 'foo';
+    $model->description = 'test';
+    $model->Insert();
+
+    $model = new TestModel();
+    $model->title = 'test';
+    $model->description = 'baz';
+    $model->Insert();
+
+    $results = TestModel::FetchGroup('title = ?', 'test');
+    $this->assertEquals(3, count($results));
+
+    $results = TestModel::FetchGroup('title = :title', array('title' => 'test'));
+    $this->assertEquals(3, count($results));
+
+    $results = TestModel::FetchGroup();
+    $this->assertEquals(4, count($results));
+  }
+
+  public function testUpdate()
+  {
+    $model = new TestModel();
+    $model->title = 'Test Update';
+    $model->description = 'foobar';
+    $model->value = 'alpha';
+    $model->Insert();
+
+    $model = new TestModel(1);
+    $model->value = 'bravo';
+    $model->Update();
+
+    $model = new TestModel(1);
+    $model->FetchInto();
+    $this->assertEquals('Test Update', $model->title);
+    $this->assertEquals('foobar', $model->description);
+    $this->assertEquals('bravo', $model->value);
+  }
+
+  public function testDelete()
+  {
+    $this->testInsert();
+    $model = new TestModel(1);
+    $model->Delete();
+
+    $model = new TestModel(2);
+    $model->FetchInto();
+    $this->assertEquals('Hello', $model->title);
+
+    $this->setExpectedException('hoplite\data\ModelException');
+    $model = new TestModel(1);
+    $model->FetchInto();
+  }
+
+  public function testCompoundBadCreate()
+  {
+    $this->setExpectedException('hoplite\data\ModelException');
+    $model = new CompoundKeyModel(1);
+  }
+
+  public function testCompoundInsert()
+  {
+    $model = new CompoundKeyModel(array('id_1' => 1, 'id_2' => 2));
+    $model->value = 'foo';
+    $model->Insert();
+  }
+
+  public function testCompoundFetch()
+  {
+    $this->testCompoundInsert();
+    $model = new CompoundKeyModel(array('id_1' => 1, 'id_2' => 2));
+    $model->FetchInto();
+    $this->assertEquals('foo', $model->value);
+  }
+
+  public function testPrefix()
+  {
+    $test = new PrefixTest();
+    $this->assertEquals('test_prefix', $test->table());
+  }
+
+  public function testSuccessfulQueryWithTMI()
+  {
+    $model = new TestModel();
+    $model->title = 'Title';
+    $model->description = 'Desc';
+    $model->value = 'Value';
+    $model->Insert();
+
+    $model = new TestModel(1);
+    $model->title = 'Title2';
+    $model->Update();
+    $data = $model->Fetch();
+    $this->assertEquals('Title2', $data->title);
+  }
+}