From ae95dfbcb2fcaa04eaed57c00bc3e4053000724d Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 30 Jul 2011 12:33:26 -0400 Subject: [PATCH] Add data\Model from phalanx --- data/model.php | 196 ++++++++++++++++++++++++++++ testing/tests/data/model_test.php | 210 ++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 data/model.php create mode 100644 testing/tests/data/model_test.php diff --git a/data/model.php b/data/model.php new file mode 100644 index 0000000..cf5979f --- /dev/null +++ b/data/model.php @@ -0,0 +1,196 @@ +. + +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 index 0000000..b223e35 --- /dev/null +++ b/testing/tests/data/model_test.php @@ -0,0 +1,210 @@ +. + +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); + } +} -- 2.22.5