--- /dev/null
+<?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 {}
--- /dev/null
+<?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);
+ }
+}