Deprecated: Assigning the return value of new by reference is deprecated in /home/bluestat/public_html/source/index.php on line 477
hoplite - Blob - ViewGit - Blue Static
<?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 {}