summaryrefslogtreecommitdiff
path: root/idiorm.php
diff options
context:
space:
mode:
Diffstat (limited to 'idiorm.php')
-rw-r--r--idiorm.php498
1 files changed, 451 insertions, 47 deletions
diff --git a/idiorm.php b/idiorm.php
index d830a2e..f809bd0 100644
--- a/idiorm.php
+++ b/idiorm.php
@@ -38,15 +38,15 @@
*
*/
- class ORM {
+ class ORM implements ArrayAccess {
// ----------------------- //
// --- CLASS CONSTANTS --- //
// ----------------------- //
- // Where condition array keys
- const WHERE_FRAGMENT = 0;
- const WHERE_VALUES = 1;
+ // WHERE and HAVING condition array keys
+ const CONDITION_FRAGMENT = 0;
+ const CONDITION_VALUES = 1;
const DEFAULT_CONNECTION = 'default';
@@ -66,6 +66,7 @@
'identifier_quote_character' => null, // if this is null, will be autodetected
'logging' => false,
'caching' => false,
+ 'return_result_sets' => false,
);
protected static $_config = array();
@@ -82,6 +83,9 @@
// Query cache, only used if query caching is enabled
protected static $_query_cache = array();
+ // Reference to previously used PDOStatement object to enable low-level access, if needed
+ protected static $_last_statement = null;
+
// --------------------------- //
// --- INSTANCE PROPERTIES --- //
// --------------------------- //
@@ -134,6 +138,9 @@
// GROUP BY
protected $_group_by = array();
+ // HAVING
+ protected $_having_conditions = array();
+
// The data for a hydrated instance of the class
protected $_data = array();
@@ -312,6 +319,29 @@
}
/**
+ * Returns the PDOStatement instance last used by any connection wrapped by the ORM.
+ * Useful for access to PDOStatement::rowCount() or error information
+ */
+ public static function get_last_statement() {
+ return self::$_last_statement;
+ }
+
+ /**
+ * Internal helper method for executing statments. Logs queries, and
+ * stores statement object in ::_last_statment, accessible publicly
+ * through ::get_last_statement()
+ * @return bool Response of PDOStatement::execute()
+ **/
+ protected static function _execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) {
+ self::_log_query($query, $parameters, $connection_name);
+ $statement = self::$_db[$connection_name]->prepare($query);
+
+ self::$_last_statement = $statement;
+
+ return $statement->execute($parameters);
+ }
+
+ /**
* Add a query to the internal query log. Only works if the
* 'logging' config option is set to true.
*
@@ -479,14 +509,39 @@
* from your query, and execute it. Will return an array
* of instances of the ORM class, or an empty array if
* no rows were returned.
+ * @return array|\IdiormResultSet
*/
public function find_many() {
+ if(self::$_config['return_result_sets']) {
+ return $this->find_result_set();
+ }
+ return $this->_find_many();
+ }
+
+ /**
+ * Tell the ORM that you are expecting multiple results
+ * from your query, and execute it. Will return an array
+ * of instances of the ORM class, or an empty array if
+ * no rows were returned.
+ * @return array
+ */
+ protected function _find_many() {
$rows = $this->_run();
return array_map(array($this, '_create_instance_from_row'), $rows);
}
/**
* Tell the ORM that you are expecting multiple results
+ * from your query, and execute it. Will return a result set object
+ * containing instances of the ORM class.
+ * @return \IdiormResultSet
+ */
+ public function find_result_set() {
+ return new IdiormResultSet($this->_find_many());
+ }
+
+ /**
+ * Tell the ORM that you are expecting multiple results
* from your query, and execute it. Will return an array,
* or an empty array if no rows were returned.
* @return array
@@ -550,7 +605,16 @@
}
$this->select_expr("$sql_function($column)", $alias);
$result = $this->find_one();
- return ($result !== false && isset($result->$alias)) ? (int) $result->$alias : 0;
+
+ $return_value = 0;
+ if($result !== false && isset($result->$alias)) {
+ if((int) $result->$alias == (float) $result->$alias) {
+ $return_value = (int) $result->$alias;
+ } else {
+ $return_value = (float) $result->$alias;
+ }
+ }
+ return $return_value;
}
/**
@@ -808,33 +872,62 @@
}
/**
+ * Internal method to add a HAVING condition to the query
+ */
+ protected function _add_having($fragment, $values=array()) {
+ return $this->_add_condition('having', $fragment, $values);
+ }
+
+ /**
+ * Internal method to add a HAVING condition to the query
+ */
+ protected function _add_simple_having($column_name, $separator, $value) {
+ return $this->_add_simple_condition('having', $column_name, $separator, $value);
+ }
+
+ /**
* Internal method to add a WHERE condition to the query
*/
protected function _add_where($fragment, $values=array()) {
+ return $this->_add_condition('where', $fragment, $values);
+ }
+
+ /**
+ * Internal method to add a WHERE condition to the query
+ */
+ protected function _add_simple_where($column_name, $separator, $value) {
+ return $this->_add_simple_condition('where', $column_name, $separator, $value);
+ }
+
+ /**
+ * Internal method to add a HAVING or WHERE condition to the query
+ */
+ protected function _add_condition($type, $fragment, $values=array()) {
+ $conditions_class_property_name = "_{$type}_conditions";
if (!is_array($values)) {
$values = array($values);
}
- $this->_where_conditions[] = array(
- self::WHERE_FRAGMENT => $fragment,
- self::WHERE_VALUES => $values,
- );
+ array_push($this->$conditions_class_property_name, array(
+ self::CONDITION_FRAGMENT => $fragment,
+ self::CONDITION_VALUES => $values,
+ ));
return $this;
}
- /**
+ /**
* Helper method to compile a simple COLUMN SEPARATOR VALUE
- * style WHERE condition into a string and value ready to
- * be passed to the _add_where method. Avoids duplication
+ * style HAVING or WHERE condition into a string and value ready to
+ * be passed to the _add_condition method. Avoids duplication
* of the call to _quote_identifier
*/
- protected function _add_simple_where($column_name, $separator, $value) {
+ protected function _add_simple_condition($type, $column_name, $separator, $value) {
// Add the table name in case of ambiguous columns
if (count($this->_join_sources) > 0 && strpos($column_name, '.') === false) {
$column_name = "{$this->_table_name}.{$column_name}";
}
$column_name = $this->_quote_identifier($column_name);
- return $this->_add_where("{$column_name} {$separator} ?", $value);
- }
+ return $this->_add_condition($type, "{$column_name} {$separator} ?", $value);
+ }
/**
* Return a string containing the given number of question marks,
@@ -1037,6 +1130,123 @@
}
/**
+ * Add a HAVING column = value clause to your query. Each time
+ * this is called in the chain, an additional HAVING will be
+ * added, and these will be ANDed together when the final query
+ * is built.
+ */
+ public function having($column_name, $value) {
+ return $this->having_equal($column_name, $value);
+ }
+
+ /**
+ * More explicitly named version of for the having() method.
+ * Can be used if preferred.
+ */
+ public function having_equal($column_name, $value) {
+ return $this->_add_simple_having($column_name, '=', $value);
+ }
+
+ /**
+ * Add a HAVING column != value clause to your query.
+ */
+ public function having_not_equal($column_name, $value) {
+ return $this->_add_simple_having($column_name, '!=', $value);
+ }
+
+ /**
+ * Special method to query the table by its primary key
+ */
+ public function having_id_is($id) {
+ return $this->having($this->_get_id_column_name(), $id);
+ }
+
+ /**
+ * Add a HAVING ... LIKE clause to your query.
+ */
+ public function having_like($column_name, $value) {
+ return $this->_add_simple_having($column_name, 'LIKE', $value);
+ }
+
+ /**
+ * Add where HAVING ... NOT LIKE clause to your query.
+ */
+ public function having_not_like($column_name, $value) {
+ return $this->_add_simple_having($column_name, 'NOT LIKE', $value);
+ }
+
+ /**
+ * Add a HAVING ... > clause to your query
+ */
+ public function having_gt($column_name, $value) {
+ return $this->_add_simple_having($column_name, '>', $value);
+ }
+
+ /**
+ * Add a HAVING ... < clause to your query
+ */
+ public function having_lt($column_name, $value) {
+ return $this->_add_simple_having($column_name, '<', $value);
+ }
+
+ /**
+ * Add a HAVING ... >= clause to your query
+ */
+ public function having_gte($column_name, $value) {
+ return $this->_add_simple_having($column_name, '>=', $value);
+ }
+
+ /**
+ * Add a HAVING ... <= clause to your query
+ */
+ public function having_lte($column_name, $value) {
+ return $this->_add_simple_having($column_name, '<=', $value);
+ }
+
+ /**
+ * Add a HAVING ... IN clause to your query
+ */
+ public function having_in($column_name, $values) {
+ $column_name = $this->_quote_identifier($column_name);
+ $placeholders = $this->_create_placeholders($values);
+ return $this->_add_having("{$column_name} IN ({$placeholders})", $values);
+ }
+
+ /**
+ * Add a HAVING ... NOT IN clause to your query
+ */
+ public function having_not_in($column_name, $values) {
+ $column_name = $this->_quote_identifier($column_name);
+ $placeholders = $this->_create_placeholders($values);
+ return $this->_add_having("{$column_name} NOT IN ({$placeholders})", $values);
+ }
+
+ /**
+ * Add a HAVING column IS NULL clause to your query
+ */
+ public function having_null($column_name) {
+ $column_name = $this->_quote_identifier($column_name);
+ return $this->_add_having("{$column_name} IS NULL");
+ }
+
+ /**
+ * Add a HAVING column IS NOT NULL clause to your query
+ */
+ public function having_not_null($column_name) {
+ $column_name = $this->_quote_identifier($column_name);
+ return $this->_add_having("{$column_name} IS NOT NULL");
+ }
+
+ /**
+ * Add a raw HAVING clause to the query. The clause should
+ * contain question mark placeholders, which will be bound
+ * to the parameters supplied in the second argument.
+ */
+ public function having_raw($clause, $parameters=array()) {
+ return $this->_add_having($clause, $parameters);
+ }
+
+ /**
* Build a SELECT statement based on the clauses that have
* been passed to this instance by chaining method calls.
*/
@@ -1055,6 +1265,7 @@
$this->_build_join(),
$this->_build_where(),
$this->_build_group_by(),
+ $this->_build_having(),
$this->_build_order_by(),
$this->_build_limit(),
$this->_build_offset(),
@@ -1094,18 +1305,14 @@
* Build the WHERE clause(s)
*/
protected function _build_where() {
- // If there are no WHERE clauses, return empty string
- if (count($this->_where_conditions) === 0) {
- return '';
- }
-
- $where_conditions = array();
- foreach ($this->_where_conditions as $condition) {
- $where_conditions[] = $condition[self::WHERE_FRAGMENT];
- $this->_values = array_merge($this->_values, $condition[self::WHERE_VALUES]);
- }
+ return $this->_build_conditions('where');
+ }
- return "WHERE " . join(" AND ", $where_conditions);
+ /**
+ * Build the HAVING clause(s)
+ */
+ protected function _build_having() {
+ return $this->_build_conditions('having');
}
/**
@@ -1119,6 +1326,27 @@
}
/**
+ * Build a WHERE or HAVING clause
+ * @param string $type
+ * @return string
+ */
+ protected function _build_conditions($type) {
+ $conditions_class_property_name = "_{$type}_conditions";
+ // If there are no clauses, return empty string
+ if (count($this->$conditions_class_property_name) === 0) {
+ return '';
+ }
+
+ $conditions = array();
+ foreach ($this->$conditions_class_property_name as $condition) {
+ $conditions[] = $condition[self::CONDITION_FRAGMENT];
+ $this->_values = array_merge($this->_values, $condition[self::CONDITION_VALUES]);
+ }
+
+ return strtoupper($type) . " " . join(" AND ", $conditions);
+ }
+
+ /**
* Build ORDER BY
*/
protected function _build_order_by() {
@@ -1185,8 +1413,14 @@
if ($part === '*') {
return $part;
}
+
$quote_character = self::$_config[$this->_connection_name]['identifier_quote_character'];
- return $quote_character . $part . $quote_character;
+ // double up any identifier quotes to escape them
+ return $quote_character .
+ str_replace($quote_character,
+ $quote_character . $quote_character,
+ $part
+ ) . $quote_character;
}
/**
@@ -1250,9 +1484,8 @@
}
}
- self::_log_query($query, $this->_values, $this->_connection_name);
- $statement = self::$_db[$this->_connection_name]->prepare($query);
- $statement->execute($this->_values);
+ self::_execute($query, $this->_values, $this->_connection_name);
+ $statement = self::get_last_statement();
$rows = array();
while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
@@ -1320,6 +1553,15 @@
$this->_set_orm_property($key, $value);
}
+ /**
+ * Set a property to a particular value on this object.
+ * To set multiple properties at once, pass an associative array
+ * as the first parameter and leave out the second parameter.
+ * Flags the properties as 'dirty' so they will be saved to the
+ * database when save() is called.
+ * @param string|array $key
+ * @param string|null $value
+ */
public function set_expr($key, $value = null) {
$this->_set_orm_property($key, $value, true);
}
@@ -1354,6 +1596,14 @@
}
/**
+ * Check whether the model was the result of a call to create() or not
+ * @return bool
+ */
+ public function is_new() {
+ return $this->_is_new;
+ }
+
+ /**
* Save any fields which have been modified on this object
* to the database.
*/
@@ -1365,7 +1615,7 @@
if (!$this->_is_new) { // UPDATE
// If there are no dirty values, do nothing
- if (count($values) == 0) {
+ if (empty($values) && empty($this->_expr_fields)) {
return true;
}
$query = $this->_build_update();
@@ -1374,15 +1624,15 @@
$query = $this->_build_insert();
}
- self::_log_query($query, $values, $this->_connection_name);
- $statement = self::$_db[$this->_connection_name]->prepare($query);
- $success = $statement->execute($values);
+ $success = self::_execute($query, $values, $this->_connection_name);
// If we've just inserted a new record, set the ID of this object
if ($this->_is_new) {
$this->_is_new = false;
if (is_null($this->id())) {
$this->_data[$this->_get_id_column_name()] =
+ self::$_db[$this->_connection_name]->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql' ?
+ self::get_last_statement()->fetchColumn() :
self::$_db[$this->_connection_name]->lastInsertId();
}
}
@@ -1424,6 +1674,11 @@
$placeholders = $this->_create_placeholders($this->_dirty_fields);
$query[] = "({$placeholders})";
+
+ if (self::$_db[$this->_connection_name]->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
+ $query[] = 'RETURNING ' . $this->_quote_identifier($this->_get_id_column_name());
+ }
+
return join(" ", $query);
}
@@ -1438,10 +1693,8 @@
$this->_quote_identifier($this->_get_id_column_name()),
"= ?",
));
- $params = array($this->id());
- self::_log_query($query, $params, $this->_connection_name);
- $statement = self::$_db[$this->_connection_name]->prepare($query);
- return $statement->execute($params);
+
+ return self::_execute($query, array($this->id()), $this->_connection_name);
}
/**
@@ -1455,30 +1708,52 @@
$this->_quote_identifier($this->_table_name),
$this->_build_where(),
));
- self::_log_query($query, $this->_values, $this->_connection_name);
- $statement = self::$_db[$this->_connection_name]->prepare($query);
- return $statement->execute($this->_values);
+
+ return self::_execute($query, $this->_values, $this->_connection_name);
}
// --------------------- //
- // --- MAGIC METHODS --- //
+ // --- ArrayAccess --- //
// --------------------- //
- public function __get($key) {
+
+ public function offsetExists($key) {
+ return isset($this->_data[$key]);
+ }
+
+ public function offsetGet($key) {
return $this->get($key);
}
- public function __set($key, $value) {
+ public function offsetSet($key, $value) {
+ if(is_null($key)) {
+ throw new InvalidArgumentException('You must specify a key/array index.');
+ }
$this->set($key, $value);
}
- public function __unset($key) {
+ public function offsetUnset($key) {
unset($this->_data[$key]);
unset($this->_dirty_fields[$key]);
}
+ // --------------------- //
+ // --- MAGIC METHODS --- //
+ // --------------------- //
+ public function __get($key) {
+ return $this->offsetGet($key);
+ }
+
+ public function __set($key, $value) {
+ $this->offsetSet($key, $value);
+ }
+
+ public function __unset($key) {
+ $this->offsetUnset($key);
+ }
+
public function __isset($key) {
- return isset($this->_data[$key]);
+ return $this->offsetExists($key);
}
}
@@ -1513,7 +1788,7 @@
* @return string
*/
public static function str_replace_outside_quotes($search, $replace, $subject) {
- return static::value($subject)->replace_outside_quotes($search, $replace);
+ return self::value($subject)->replace_outside_quotes($search, $replace);
}
/**
@@ -1587,6 +1862,135 @@
}
/**
+ * A result set class for working with collections of model instances
+ * @author Simon Holywell <[email protected]>
+ */
+ class IdiormResultSet implements Countable, IteratorAggregate, ArrayAccess, Serializable {
+ /**
+ * The current result set as an array
+ * @var array
+ */
+ protected $_results = array();
+
+ /**
+ * Optionally set the contents of the result set by passing in array
+ * @param array $results
+ */
+ public function __construct(array $results = array()) {
+ $this->set_results($results);
+ }
+
+ /**
+ * Set the contents of the result set by passing in array
+ * @param array $results
+ */
+ public function set_results(array $results) {
+ $this->_results = $results;
+ }
+
+ /**
+ * Get the current result set as an array
+ * @return array
+ */
+ public function get_results() {
+ return $this->_results;
+ }
+
+ /**
+ * Get the current result set as an array
+ * @return array
+ */
+ public function as_array() {
+ return $this->get_results();
+ }
+
+ /**
+ * Get the number of records in the result set
+ * @return int
+ */
+ public function count() {
+ return count($this->_results);
+ }
+
+ /**
+ * Get an iterator for this object. In this case it supports foreaching
+ * over the result set.
+ * @return \ArrayIterator
+ */
+ public function getIterator() {
+ return new ArrayIterator($this->_results);
+ }
+
+ /**
+ * ArrayAccess
+ * @param int|string $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+ return isset($this->_results[$offset]);
+ }
+
+ /**
+ * ArrayAccess
+ * @param int|string $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+ return $this->_results[$offset];
+ }
+
+ /**
+ * ArrayAccess
+ * @param int|string $offset
+ * @param mixed $value
+ */
+ public function offsetSet($offset, $value) {
+ $this->_results[$offset] = $value;
+ }
+
+ /**
+ * ArrayAccess
+ * @param int|string $offset
+ */
+ public function offsetUnset($offset) {
+ unset($this->_results[$offset]);
+ }
+
+ /**
+ * Serializable
+ * @return string
+ */
+ public function serialize() {
+ return serialize($this->_results);
+ }
+
+ /**
+ * Serializable
+ * @param string $serialized
+ * @return array
+ */
+ public function unserialize($serialized) {
+ return unserialize($serialized);
+ }
+
+ /**
+ * Call a method on all models in a result set. This allows for method
+ * chaining such as setting a property on all models in a result set or
+ * any other batch operation across models.
+ * @example ORM::for_table('Widget')->find_many()->set('field', 'value')->save();
+ * @param string $method
+ * @param array $params
+ * @return \IdiormResultSet
+ */
+ public function __call($method, $params = array()) {
+ foreach($this->_results as $model) {
+ call_user_func_array(array($model, $method), $params);
+ }
+ return $this;
+ }
+ }
+
+ /**
* A placeholder for exceptions eminating from the IdiormString class
*/
class IdiormStringException extends Exception {}