ult'])){ return call_user_func_array(self::$_config[$connection_name]['cache_query_result'], array($cache_key, $value, $table_name, $connection_name)); } elseif (!isset(self::$_query_cache[$connection_name])) { self::$_query_cache[$connection_name] = array(); } self::$_query_cache[$connection_name][$cache_key] = $value; } /** * Execute the SELECT query that has been built up by chaining methods * on this class. Return an array of rows as associative arrays. */ protected function _run() { $query = $this->_build_select(); $caching_enabled = self::$_config[$this->_connection_name]['caching']; if ($caching_enabled) { $cache_key = self::_create_cache_key($query, $this->_values, $this->_table_name, $this->_connection_name); $cached_result = self::_check_query_cache($cache_key, $this->_table_name, $this->_connection_name); if ($cached_result !== false) { $this->_reset_idiorm_state(); return $cached_result; } } self::_execute($query, $this->_values, $this->_connection_name); $statement = self::get_last_statement(); $rows = array(); while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { $rows[] = $row; } if ($caching_enabled) { self::_cache_query_result($cache_key, $rows, $this->_table_name, $this->_connection_name); } $this->_reset_idiorm_state(); return $rows; } /** * Reset the Idiorm instance state */ private function _reset_idiorm_state() { $this->_values = array(); $this->_result_columns = array('*'); $this->_using_default_result_columns = true; } /** * Return the raw data wrapped by this ORM * instance as an associative array. Column * names may optionally be supplied as arguments, * if so, only those keys will be returned. */ public function as_array() { if (func_num_args() === 0) { return $this->_data; } $args = func_get_args(); return array_intersect_key($this->_data, array_flip($args)); } /** * Return the value of a property of this object (database row) * or null if not present. * * If a column-names array is passed, it will return a associative array * with the value of each column or null if it is not present. */ public function get($key) { if (is_array($key)) { $result = array(); foreach($key as $column) { $result[$column] = isset($this->_data[$column]) ? $this->_data[$column] : null; } return $result; } else { return isset($this->_data[$key]) ? $this->_data[$key] : null; } } /** * Return the name of the column in the database table which contains * the primary key ID of the row. */ protected function _get_id_column_name() { if (!is_null($this->_instance_id_column)) { return $this->_instance_id_column; } if (isset(self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name])) { return self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name]; } return self::$_config[$this->_connection_name]['id_column']; } /** * Get the primary key ID of this object. */ public function id($disallow_null = false) { $id = $this->get($this->_get_id_column_name()); if ($disallow_null) { if (is_array($id)) { foreach ($id as $id_part) { if ($id_part === null) { throw new Exception('Primary key ID contains null value(s)'); } } } else if ($id === null) { throw new Exception('Primary key ID missing from row or is null'); } } return $id; } /** * 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. */ public function set($key, $value = null) { return $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) { return $this->_set_orm_property($key, $value, true); } /** * Set a property on the ORM object. * @param string|array $key * @param string|null $value * @param bool $raw Whether this value should be treated as raw or not */ protected function _set_orm_property($key, $value = null, $expr = false) { if (!is_array($key)) { $key = array($key => $value); } foreach ($key as $field => $value) { $this->_data[$field] = $value; $this->_dirty_fields[$field] = $value; if (false === $expr and isset($this->_expr_fields[$field])) { unset($this->_expr_fields[$field]); } else if (true === $expr) { $this->_expr_fields[$field] = true; } } return $this; } /** * Check whether the given field has been changed since this * object was saved. */ public function is_dirty($key) { return array_key_exists($key, $this->_dirty_fields); } /** * 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. */ public function save() { $query = array(); // remove any expression fields as they are already baked into the query $values = array_values(array_diff_key($this->_dirty_fields, $this->_expr_fields)); if (!$this->_is_new) { // UPDATE // If there are no dirty values, do nothing if (empty($values) && empty($this->_expr_fields)) { return true; } $query = $this->_build_update(); $id = $this->id(true); if (is_array($id)) { $values = array_merge($values, array_values($id)); } else { $values[] = $id; } } else { // INSERT $query = $this->_build_insert(); } $success = self::_execute($query, $values, $this->_connection_name); $caching_auto_clear_enabled = self::$_config[$this->_connection_name]['caching_auto_clear']; if($caching_auto_clear_enabled){ self::clear_cache($this->_table_name, $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 ($this->count_null_id_columns() != 0) { $db = self::get_db($this->_connection_name); if($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { // it may return several columns if a compound primary // key is used $row = self::get_last_statement()->fetch(PDO::FETCH_ASSOC); foreach($row as $key => $value) { $this->_data[$key] = $value; } } else { $column = $this->_get_id_column_name(); // if the primary key is compound, assign the last inserted id // to the first column if (is_array($column)) { $column = reset($column); } $this->_data[$column] = $db->lastInsertId(); } } } $this->_dirty_fields = $this->_expr_fields = array(); return $success; } /** * Add a WHERE clause for every column that belongs to the primary key */ public function _add_id_column_conditions(&$query) { $query[] = "WHERE"; $keys = is_array($this->_get_id_column_name()) ? $this->_get_id_column_name() : array( $this->_get_id_column_name() ); $first = true; foreach($keys as $key) { if ($first) { $first = false; } else { $query[] = "AND"; } $query[] = $this->_quote_identifier($key); $query[] = "= ?"; } } /** * Build an UPDATE query */ protected function _build_update() { $query = array(); $query[] = "UPDATE {$this->_quote_identifier($this->_table_name)} SET"; $field_list = array(); foreach ($this->_dirty_fields as $key => $value) { if(!array_key_exists($key, $this->_expr_fields)) { $value = '?'; } $field_list[] = "{$this->_quote_identifier($key)} = $value"; } $query[] = implode(', ', $field_list); $this->_add_id_column_conditions($query); return implode(' ', $query); } /** * Build an INSERT query */ protected function _build_insert() { $query[] = "INSERT INTO"; $query[] = $this->_quote_identifier($this->_table_name); $field_list = array_map(array($this, '_quote_identifier'), array_keys($this->_dirty_fields)); $query[] = "(" . implode(', ', $field_list) . ")"; $query[] = "VALUES"; $placeholders = $this->_create_placeholders($this->_dirty_fields); $query[] = "({$placeholders})"; if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') { $query[] = 'RETURNING ' . $this->_quote_identifier($this->_get_id_column_name()); } return implode(' ', $query); } /** * Delete this record from the database */ public function delete() { $query = array( "DELETE FROM", $this->_quote_identifier($this->_table_name) ); $this->_add_id_column_conditions($query); return self::_execute(implode(' ', $query), is_array($this->id(true)) ? array_values($this->id(true)) : array($this->id(true)), $this->_connection_name); } /** * Delete many records from the database */ public function delete_many() { // Build and return the full DELETE statement by concatenating // the results of calling each separate builder method. $query = $this->_join_if_not_empty(" ", array( "DELETE FROM", $this->_quote_identifier($this->_table_name), $this->_build_where(), )); return self::_execute($query, $this->_values, $this->_connection_name); } // --------------------- // // --- ArrayAccess --- // // --------------------- // public function offsetExists($key): bool { return array_key_exists($key, $this->_data); } #[\ReturnTypeWillChange] // Need to use annotation since mixed was added in 8.0 public function offsetGet($key) { return $this->get($key); } public function offsetSet($key, $value): void { if(is_null($key)) { throw new InvalidArgumentException('You must specify a key/array index.'); } $this->set($key, $value); } public function offsetUnset($key): void { 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 $this->offsetExists($key); } /** * Magic method to capture calls to undefined class methods. * In this case we are attempting to convert camel case formatted * methods into underscore formatted methods. * * This allows us to call ORM methods using camel case and remain * backwards compatible. * * @param string $name * @param array $arguments * @return ORM */ public function __call($name, $arguments) { $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); if (method_exists($this, $method)) { return call_user_func_array(array($this, $method), $arguments); } else { throw new IdiormMethodMissingException("Method $name() does not exist in class " . get_class($this)); } } /** * Magic method to capture calls to undefined static class methods. * In this case we are attempting to convert camel case formatted * methods into underscore formatted methods. * * This allows us to call ORM methods using camel case and remain * backwards compatible. * * @param string $name * @param array $arguments * @return ORM */ public static function __callStatic($name, $arguments) { $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)); return call_user_func_array(array('MailPoetVendor\Idiorm\ORM', $method), $arguments); } } /** * A class to handle str_replace operations that involve quoted strings * @example IdiormString::str_replace_outside_quotes('?', '%s', 'columnA = "Hello?" AND columnB = ?'); * @example IdiormString::value('columnA = "Hello?" AND columnB = ?')->replace_outside_quotes('?', '%s'); * @author Jeff Roberson * @author Simon Holywell * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer */ class IdiormString { protected $subject; protected $search; protected $replace; /** * Get an easy to use instance of the class * @param string $subject * @return \self */ public static function value($subject) { return new self($subject); } /** * Shortcut method: Replace all occurrences of the search string with the replacement * string where they appear outside quotes. * @param string $search * @param string $replace * @param string $subject * @return string */ public static function str_replace_outside_quotes($search, $replace, $subject) { return self::value($subject)->replace_outside_quotes($search, $replace); } /** * Set the base string object * @param string $subject */ public function __construct($subject) { $this->subject = (string) $subject; } /** * Replace all occurrences of the search string with the replacement * string where they appear outside quotes * @param string $search * @param string $replace * @return string */ public function replace_outside_quotes($search, $replace) { $this->search = $search; $this->replace = $replace; return $this->_str_replace_outside_quotes(); } /** * Validate an input string and perform a replace on all ocurrences * of $this->search with $this->replace * @author Jeff Roberson * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer * @return string */ protected function _str_replace_outside_quotes(){ $re_valid = '/ # Validate string having embedded quoted substrings. ^ # Anchor to start of string. (?: # Zero or more string chunks. "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # Either a double quoted chunk, | \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' # or a single quoted chunk, | [^\'"\\\\]+ # or an unquoted chunk (no escapes). )* # Zero or more string chunks. \z # Anchor to end of string. /sx'; if (!preg_match($re_valid, $this->subject)) { throw new IdiormStringException("Subject string is not valid in the replace_outside_quotes context."); } $re_parse = '/ # Match one chunk of a valid string having embedded quoted substrings. ( # Either $1: Quoted chunk. "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # Either a double quoted chunk, | \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' # or a single quoted chunk. ) # End $1: Quoted chunk. | ([^\'"\\\\]+) # or $2: an unquoted chunk (no escapes). /sx'; return preg_replace_callback($re_parse, array($this, '_str_replace_outside_quotes_cb'), $this->subject); } /** * Process each matching chunk from preg_replace_callback replacing * each occurrence of $this->search with $this->replace * @author Jeff Roberson * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer * @param array $matches * @return string */ protected function _str_replace_outside_quotes_cb($matches) { // Return quoted string chunks (in group $1) unaltered. if ($matches[1]) return $matches[1]; // Process only unquoted chunks (in group $2). return preg_replace('/'. preg_quote($this->search, '/') .'/', $this->replace, $matches[2]); } } /** * A result set class for working with collections of model instances * @author Simon Holywell * @method null setResults(array $results) * @method array getResults() */ class IdiormResultSet implements Countable, IteratorAggregate, ArrayAccess { /** * 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(): int { 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(): \Traversable { return new ArrayIterator($this->_results); } /** * ArrayAccess * @param int|string $offset * @return bool */ public function offsetExists($offset): bool { return isset($this->_results[$offset]); } /** * ArrayAccess * @param int|string $offset * @return mixed */ #[\ReturnTypeWillChange] // Can't use mixed return typehint since it was added in 8.0 public function offsetGet($offset) { return $this->_results[$offset]; } /** * ArrayAccess * @param int|string $offset * @param mixed $value */ public function offsetSet($offset, $value): void { $this->_results[$offset] = $value; } /** * ArrayAccess * @param int|string $offset */ public function offsetUnset($offset): void { unset($this->_results[$offset]); } /** * Serializable * @return string */ public function serialize() { return serialize($this->_results); } public function __serialize() { return $this->serialize(); } /** * Serializable * @param string $serialized * @return array */ public function unserialize($serialized) { return unserialize($serialized); } public function __unserialize($serialized) { return $this->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) { if (method_exists($model, $method)) { call_user_func_array(array($model, $method), $params); } else { throw new IdiormMethodMissingException("Method $method() does not exist in class " . get_class($this)); } } return $this; } } /** * A placeholder for exceptions eminating from the IdiormString class */ class IdiormStringException extends Exception {} class IdiormMethodMissingException extends Exception {}