PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 4.21.0
MailPoet – Newsletters, Email Marketing, and Automation v4.21.0
5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib-3rd-party / Idiorm / idiorm.php
mailpoet / lib-3rd-party / Idiorm Last commit date
idiorm.php 3 years ago index.php 3 years ago
idiorm.php
2570 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoetVendor\Idiorm;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use ArrayAccess;
9 use ArrayIterator;
10 use Countable;
11 use Exception;
12 use InvalidArgumentException;
13 use IteratorAggregate;
14 use PDO;
15 use PDOStatement;
16
17 /**
18 *
19 * Idiorm
20 *
21 * http://github.com/j4mie/idiorm/
22 *
23 * A single-class super-simple database abstraction layer for PHP.
24 * Provides (nearly) zero-configuration object-relational mapping
25 * and a fluent interface for building basic, commonly-used queries.
26 *
27 * BSD Licensed.
28 *
29 * Copyright (c) 2010, Jamie Matthews
30 * All rights reserved.
31 *
32 * Redistribution and use in source and binary forms, with or without
33 * modification, are permitted provided that the following conditions are met:
34 *
35 * * Redistributions of source code must retain the above copyright notice, this
36 * list of conditions and the following disclaimer.
37 *
38 * * Redistributions in binary form must reproduce the above copyright notice,
39 * this list of conditions and the following disclaimer in the documentation
40 * and/or other materials provided with the distribution.
41 *
42 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
43 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
44 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
45 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
46 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
47 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
48 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
49 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
51 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
52 *
53 *
54 * The methods documented below are magic methods that conform to PSR-1.
55 * This documentation exposes these methods to doc generators and IDEs.
56 * @see http://www.php-fig.org/psr/psr-1/
57 *
58 * @method static array|string getConfig($key = null, $connection_name = self::DEFAULT_CONNECTION)
59 * @method static null resetConfig()
60 * @method static ORM forTable($table_name, $connection_name = self::DEFAULT_CONNECTION)
61 * @method static null setDb($db, $connection_name = self::DEFAULT_CONNECTION)
62 * @method static null resetDb()
63 * @method static null setupLimitClauseStyle($connection_name)
64 * @method static PDO getDb($connection_name = self::DEFAULT_CONNECTION)
65 * @method static bool rawExecute($query, $parameters = array())
66 * @method static PDOStatement getLastStatement()
67 * @method static string getLastQuery($connection_name = null)
68 * @method static array getQueryLog($connection_name = self::DEFAULT_CONNECTION)
69 * @method array getConnectionNames()
70 * @method $this useIdColumn($id_column)
71 * @method ORM|bool findOne($id=null)
72 * @method array|IdiormResultSet findMany()
73 * @method IdiormResultSet findResultSet()
74 * @method array findArray()
75 * @method $this forceAllDirty()
76 * @method $this rawQuery($query, $parameters = array())
77 * @method $this tableAlias($alias)
78 * @method int countNullIdColumns()
79 * @method $this selectExpr($expr, $alias=null)
80 * @method ORM selectMany($values)
81 * @method ORM selectManyExpr($values)
82 * @method $this rawJoin($table, $constraint, $table_alias, $parameters = array())
83 * @method $this innerJoin($table, $constraint, $table_alias=null)
84 * @method $this leftOuterJoin($table, $constraint, $table_alias=null)
85 * @method $this rightOuterJoin($table, $constraint, $table_alias=null)
86 * @method $this fullOuterJoin($table, $constraint, $table_alias=null)
87 * @method $this whereEqual($column_name, $value=null)
88 * @method $this whereNotEqual($column_name, $value=null)
89 * @method $this whereIdIs($id)
90 * @method $this whereAnyIs($values, $operator='=')
91 * @method array|string whereIdIn($ids)
92 * @method $this whereLike($column_name, $value=null)
93 * @method $this whereNotLike($column_name, $value=null)
94 * @method $this whereGt($column_name, $value=null)
95 * @method $this whereLt($column_name, $value=null)
96 * @method $this whereGte($column_name, $value=null)
97 * @method $this whereLte($column_name, $value=null)
98 * @method $this whereIn($column_name, $values)
99 * @method $this whereNotIn($column_name, $values)
100 * @method $this whereNull($column_name)
101 * @method $this whereNotNull($column_name)
102 * @method $this whereRaw($clause, $parameters=array())
103 * @method $this orderByDesc($column_name)
104 * @method $this orderByAsc($column_name)
105 * @method $this orderByExpr($clause)
106 * @method $this groupBy($column_name)
107 * @method $this groupByExpr($expr)
108 * @method $this havingEqual($column_name, $value=null)
109 * @method $this havingNotEqual($column_name, $value=null)
110 * @method $this havingIdIs($id)
111 * @method $this havingLike($column_name, $value=null)
112 * @method $this havingNotLike($column_name, $value=null)
113 * @method $this havingGt($column_name, $value=null)
114 * @method $this havingLt($column_name, $value=null)
115 * @method $this havingGte($column_name, $value=null)
116 * @method $this havingLte($column_name, $value=null)
117 * @method $this havingIn($column_name, $values=null)
118 * @method $this havingNotIn($column_name, $values=null)
119 * @method $this havingNull($column_name)
120 * @method $this havingNotNull($column_name)
121 * @method $this havingRaw($clause, $parameters=array())
122 * @method static this clearCache($table_name = null, $connection_name = self::DEFAULT_CONNECTION)
123 * @method array asArray()
124 * @method bool setExpr($key, $value = null)
125 * @method bool isDirty($key)
126 * @method bool isNew()
127 */
128
129 class ORM implements ArrayAccess {
130
131 // ----------------------- //
132 // --- CLASS CONSTANTS --- //
133 // ----------------------- //
134
135 // WHERE and HAVING condition array keys
136 const CONDITION_FRAGMENT = 0;
137 const CONDITION_VALUES = 1;
138
139 const DEFAULT_CONNECTION = 'default';
140
141 // Limit clause style
142 const LIMIT_STYLE_TOP_N = "top";
143 const LIMIT_STYLE_LIMIT = "limit";
144
145 // ------------------------ //
146 // --- CLASS PROPERTIES --- //
147 // ------------------------ //
148
149 // Class configuration
150 protected static $_default_config = array(
151 'connection_string' => 'sqlite::memory:',
152 'id_column' => 'id',
153 'id_column_overrides' => array(),
154 'error_mode' => PDO::ERRMODE_EXCEPTION,
155 'username' => null,
156 'password' => null,
157 'driver_options' => null,
158 'identifier_quote_character' => null, // if this is null, will be autodetected
159 'limit_clause_style' => null, // if this is null, will be autodetected
160 'logging' => false,
161 'logger' => null,
162 'caching' => false,
163 'caching_auto_clear' => false,
164 'return_result_sets' => false,
165 );
166
167 // Map of configuration settings
168 protected static $_config = array();
169
170 // Map of database connections, instances of the PDO class
171 protected static $_db = array();
172
173 // Last query run, only populated if logging is enabled
174 protected static $_last_query;
175
176 // Log of all queries run, mapped by connection key, only populated if logging is enabled
177 protected static $_query_log = array();
178
179 // Query cache, only used if query caching is enabled
180 protected static $_query_cache = array();
181
182 // Reference to previously used PDOStatement object to enable low-level access, if needed
183 protected static $_last_statement = null;
184
185 // --------------------------- //
186 // --- INSTANCE PROPERTIES --- //
187 // --------------------------- //
188
189 // Key name of the connections in self::$_db used by this instance
190 protected $_connection_name;
191
192 // The name of the table the current ORM instance is associated with
193 protected $_table_name;
194
195 // Alias for the table to be used in SELECT queries
196 protected $_table_alias = null;
197
198 // Values to be bound to the query
199 protected $_values = array();
200
201 // Columns to select in the result
202 protected $_result_columns = array('*');
203
204 // Are we using the default result column or have these been manually changed?
205 protected $_using_default_result_columns = true;
206
207 // Join sources
208 protected $_join_sources = array();
209
210 // Should the query include a DISTINCT keyword?
211 protected $_distinct = false;
212
213 // Is this a raw query?
214 protected $_is_raw_query = false;
215
216 // The raw query
217 protected $_raw_query = '';
218
219 // The raw query parameters
220 protected $_raw_parameters = array();
221
222 // Array of WHERE clauses
223 protected $_where_conditions = array();
224
225 // LIMIT
226 protected $_limit = null;
227
228 // OFFSET
229 protected $_offset = null;
230
231 // ORDER BY
232 protected $_order_by = array();
233
234 // GROUP BY
235 protected $_group_by = array();
236
237 // HAVING
238 protected $_having_conditions = array();
239
240 // The data for a hydrated instance of the class
241 protected $_data = array();
242
243 // Fields that have been modified during the
244 // lifetime of the object
245 protected $_dirty_fields = array();
246
247 // Fields that are to be inserted in the DB raw
248 protected $_expr_fields = array();
249
250 // Is this a new object (has create() been called)?
251 protected $_is_new = false;
252
253 // Name of the column to use as the primary key for
254 // this instance only. Overrides the config settings.
255 protected $_instance_id_column = null;
256
257 // ---------------------- //
258 // --- STATIC METHODS --- //
259 // ---------------------- //
260
261 /**
262 * Pass configuration settings to the class in the form of
263 * key/value pairs. As a shortcut, if the second argument
264 * is omitted and the key is a string, the setting is
265 * assumed to be the DSN string used by PDO to connect
266 * to the database (often, this will be the only configuration
267 * required to use Idiorm). If you have more than one setting
268 * you wish to configure, another shortcut is to pass an array
269 * of settings (and omit the second argument).
270 * @param string|array $key
271 * @param mixed $value
272 * @param string $connection_name Which connection to use
273 */
274 public static function configure($key, $value = null, $connection_name = self::DEFAULT_CONNECTION) {
275 self::_setup_db_config($connection_name); //ensures at least default config is set
276
277 if (is_array($key)) {
278 // Shortcut: If only one array argument is passed,
279 // assume it's an array of configuration settings
280 foreach ($key as $conf_key => $conf_value) {
281 self::configure($conf_key, $conf_value, $connection_name);
282 }
283 } else {
284 if (is_null($value)) {
285 // Shortcut: If only one string argument is passed,
286 // assume it's a connection string
287 $value = $key;
288 $key = 'connection_string';
289 }
290 self::$_config[$connection_name][$key] = $value;
291 }
292 }
293
294 /**
295 * Retrieve configuration options by key, or as whole array.
296 * @param string $key
297 * @param string $connection_name Which connection to use
298 */
299 public static function get_config($key = null, $connection_name = self::DEFAULT_CONNECTION) {
300 if ($key) {
301 return self::$_config[$connection_name][$key];
302 } else {
303 return self::$_config[$connection_name];
304 }
305 }
306
307 /**
308 * Delete all configs in _config array.
309 */
310 public static function reset_config() {
311 self::$_config = array();
312 }
313
314 /**
315 * Despite its slightly odd name, this is actually the factory
316 * method used to acquire instances of the class. It is named
317 * this way for the sake of a readable interface, ie
318 * ORM::for_table('table_name')->find_one()-> etc. As such,
319 * this will normally be the first method called in a chain.
320 * @param string $table_name
321 * @param string $connection_name Which connection to use
322 * @return ORM
323 */
324 public static function for_table($table_name, $connection_name = self::DEFAULT_CONNECTION) {
325 self::_setup_db($connection_name);
326 return new self($table_name, array(), $connection_name);
327 }
328
329 /**
330 * Set up the database connection used by the class
331 * @param string $connection_name Which connection to use
332 */
333 protected static function _setup_db($connection_name = self::DEFAULT_CONNECTION) {
334 if (!array_key_exists($connection_name, self::$_db) ||
335 !is_object(self::$_db[$connection_name])) {
336 self::_setup_db_config($connection_name);
337
338 $db = new PDO(
339 self::$_config[$connection_name]['connection_string'],
340 self::$_config[$connection_name]['username'],
341 self::$_config[$connection_name]['password'],
342 self::$_config[$connection_name]['driver_options']
343 );
344
345 $db->setAttribute(PDO::ATTR_ERRMODE, self::$_config[$connection_name]['error_mode']);
346 self::set_db($db, $connection_name);
347 }
348 }
349
350 /**
351 * Ensures configuration (multiple connections) is at least set to default.
352 * @param string $connection_name Which connection to use
353 */
354 protected static function _setup_db_config($connection_name) {
355 if (!array_key_exists($connection_name, self::$_config)) {
356 self::$_config[$connection_name] = self::$_default_config;
357 }
358 }
359
360 /**
361 * Set the PDO object used by Idiorm to communicate with the database.
362 * This is public in case the ORM should use a ready-instantiated
363 * PDO object as its database connection. Accepts an optional string key
364 * to identify the connection if multiple connections are used.
365 * @param PDO $db
366 * @param string $connection_name Which connection to use
367 */
368 public static function set_db($db, $connection_name = self::DEFAULT_CONNECTION) {
369 self::_setup_db_config($connection_name);
370 self::$_db[$connection_name] = $db;
371 if(!is_null(self::$_db[$connection_name])) {
372 self::_setup_identifier_quote_character($connection_name);
373 self::_setup_limit_clause_style($connection_name);
374 }
375 }
376
377 /**
378 * Close and delete all registered PDO objects in _db array.
379 */
380 public static function reset_db() {
381 self::$_db = null;
382
383 self::$_db = array();
384 }
385
386 /**
387 * Detect and initialise the character used to quote identifiers
388 * (table names, column names etc). If this has been specified
389 * manually using ORM::configure('identifier_quote_character', 'some-char'),
390 * this will do nothing.
391 * @param string $connection_name Which connection to use
392 */
393 protected static function _setup_identifier_quote_character($connection_name) {
394 if (is_null(self::$_config[$connection_name]['identifier_quote_character'])) {
395 self::$_config[$connection_name]['identifier_quote_character'] =
396 self::_detect_identifier_quote_character($connection_name);
397 }
398 }
399
400 /**
401 * Detect and initialise the limit clause style ("SELECT TOP 5" /
402 * "... LIMIT 5"). If this has been specified manually using
403 * ORM::configure('limit_clause_style', 'top'), this will do nothing.
404 * @param string $connection_name Which connection to use
405 */
406 public static function _setup_limit_clause_style($connection_name) {
407 if (is_null(self::$_config[$connection_name]['limit_clause_style'])) {
408 self::$_config[$connection_name]['limit_clause_style'] =
409 self::_detect_limit_clause_style($connection_name);
410 }
411 }
412
413 /**
414 * Return the correct character used to quote identifiers (table
415 * names, column names etc) by looking at the driver being used by PDO.
416 * @param string $connection_name Which connection to use
417 * @return string
418 */
419 protected static function _detect_identifier_quote_character($connection_name) {
420 switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) {
421 case 'pgsql':
422 case 'sqlsrv':
423 case 'dblib':
424 case 'mssql':
425 case 'sybase':
426 case 'firebird':
427 return '"';
428 case 'mysql':
429 case 'sqlite':
430 case 'sqlite2':
431 default:
432 return '`';
433 }
434 }
435
436 /**
437 * Returns a constant after determining the appropriate limit clause
438 * style
439 * @param string $connection_name Which connection to use
440 * @return string Limit clause style keyword/constant
441 */
442 protected static function _detect_limit_clause_style($connection_name) {
443 switch(self::get_db($connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME)) {
444 case 'sqlsrv':
445 case 'dblib':
446 case 'mssql':
447 return ORM::LIMIT_STYLE_TOP_N;
448 default:
449 return ORM::LIMIT_STYLE_LIMIT;
450 }
451 }
452
453 /**
454 * Returns the PDO instance used by the the ORM to communicate with
455 * the database. This can be called if any low-level DB access is
456 * required outside the class. If multiple connections are used,
457 * accepts an optional key name for the connection.
458 * @param string $connection_name Which connection to use
459 * @return PDO
460 */
461 public static function get_db($connection_name = self::DEFAULT_CONNECTION) {
462 self::_setup_db($connection_name); // required in case this is called before Idiorm is instantiated
463 return self::$_db[$connection_name];
464 }
465
466 /**
467 * Executes a raw query as a wrapper for PDOStatement::execute.
468 * Useful for queries that can't be accomplished through Idiorm,
469 * particularly those using engine-specific features.
470 * @example raw_execute('SELECT `name`, AVG(`order`) FROM `customer` GROUP BY `name` HAVING AVG(`order`) > 10')
471 * @example raw_execute('INSERT OR REPLACE INTO `widget` (`id`, `name`) SELECT `id`, `name` FROM `other_table`')
472 * @param string $query The raw SQL query
473 * @param array $parameters Optional bound parameters
474 * @param string $connection_name Which connection to use
475 * @return bool Success
476 */
477 public static function raw_execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) {
478 self::_setup_db($connection_name);
479 return self::_execute($query, $parameters, $connection_name);
480 }
481
482 /**
483 * Returns the PDOStatement instance last used by any connection wrapped by the ORM.
484 * Useful for access to PDOStatement::rowCount() or error information
485 * @return PDOStatement
486 */
487 public static function get_last_statement() {
488 return self::$_last_statement;
489 }
490
491 /**
492 * Internal helper method for executing statments. Logs queries, and
493 * stores statement object in ::_last_statment, accessible publicly
494 * through ::get_last_statement()
495 * @param string $query
496 * @param array $parameters An array of parameters to be bound in to the query
497 * @param string $connection_name Which connection to use
498 * @return bool Response of PDOStatement::execute()
499 */
500 protected static function _execute($query, $parameters = array(), $connection_name = self::DEFAULT_CONNECTION) {
501 $statement = self::get_db($connection_name)->prepare($query);
502 self::$_last_statement = $statement;
503 $time = microtime(true);
504
505 foreach ($parameters as $key => &$param) {
506 if (is_null($param)) {
507 $type = PDO::PARAM_NULL;
508 } else if (is_bool($param)) {
509 $type = PDO::PARAM_BOOL;
510 } else if (is_int($param)) {
511 $type = PDO::PARAM_INT;
512 } else {
513 $type = PDO::PARAM_STR;
514 }
515
516 $statement->bindParam(is_int($key) ? ++$key : $key, $param, $type);
517 }
518
519 $q = $statement->execute();
520 self::_log_query($query, $parameters, $connection_name, (microtime(true)-$time));
521
522 return $q;
523 }
524
525 /**
526 * Add a query to the internal query log. Only works if the
527 * 'logging' config option is set to true.
528 *
529 * This works by manually binding the parameters to the query - the
530 * query isn't executed like this (PDO normally passes the query and
531 * parameters to the database which takes care of the binding) but
532 * doing it this way makes the logged queries more readable.
533 * @param string $query
534 * @param array $parameters An array of parameters to be bound in to the query
535 * @param string $connection_name Which connection to use
536 * @param float $query_time Query time
537 * @return bool
538 */
539 protected static function _log_query($query, $parameters, $connection_name, $query_time) {
540 // If logging is not enabled, do nothing
541 if (!self::$_config[$connection_name]['logging']) {
542 return false;
543 }
544
545 if (!isset(self::$_query_log[$connection_name])) {
546 self::$_query_log[$connection_name] = array();
547 }
548
549 if (empty($parameters)) {
550 $bound_query = $query;
551 } else {
552 // Escape the parameters
553 $parameters = array_map(function($parameter) use ($connection_name) {
554 // Escape the parameter only when it is not empty value due to compatibility in PHP8.1
555 if ($parameter) {
556 return self::get_db($connection_name)->quote($parameter);
557 }
558 return $parameter;
559 }, $parameters);
560
561 if (array_values($parameters) === $parameters) {
562 // ? placeholders
563 // Avoid %format collision for vsprintf
564 $query = str_replace("%", "%%", $query);
565
566 // Replace placeholders in the query for vsprintf
567 if(false !== strpos($query, "'") || false !== strpos($query, '"')) {
568 $query = IdiormString::str_replace_outside_quotes("?", "%s", $query);
569 } else {
570 $query = str_replace("?", "%s", $query);
571 }
572
573 // Replace the question marks in the query with the parameters
574 $bound_query = vsprintf($query, $parameters);
575 } else {
576 // named placeholders
577 foreach ($parameters as $key => $val) {
578 $query = str_replace($key, $val, $query);
579 }
580 $bound_query = $query;
581 }
582 }
583
584 self::$_last_query = $bound_query;
585 self::$_query_log[$connection_name][] = $bound_query;
586
587
588 if(is_callable(self::$_config[$connection_name]['logger'])){
589 $logger = self::$_config[$connection_name]['logger'];
590 $logger($bound_query, $query_time);
591 }
592
593 return true;
594 }
595
596 /**
597 * Get the last query executed. Only works if the
598 * 'logging' config option is set to true. Otherwise
599 * this will return null. Returns last query from all connections if
600 * no connection_name is specified
601 * @param null|string $connection_name Which connection to use
602 * @return string
603 */
604 public static function get_last_query($connection_name = null) {
605 if ($connection_name === null) {
606 return self::$_last_query;
607 }
608 if (!isset(self::$_query_log[$connection_name])) {
609 return '';
610 }
611
612 return end(self::$_query_log[$connection_name]);
613 }
614
615 /**
616 * Get an array containing all the queries run on a
617 * specified connection up to now.
618 * Only works if the 'logging' config option is
619 * set to true. Otherwise, returned array will be empty.
620 * @param string $connection_name Which connection to use
621 */
622 public static function get_query_log($connection_name = self::DEFAULT_CONNECTION) {
623 if (isset(self::$_query_log[$connection_name])) {
624 return self::$_query_log[$connection_name];
625 }
626 return array();
627 }
628
629 /**
630 * Get a list of the available connection names
631 * @return array
632 */
633 public static function get_connection_names() {
634 return array_keys(self::$_db);
635 }
636
637 // ------------------------ //
638 // --- INSTANCE METHODS --- //
639 // ------------------------ //
640
641 /**
642 * "Private" constructor; shouldn't be called directly.
643 * Use the ORM::for_table factory method instead.
644 */
645 protected function __construct($table_name, $data = array(), $connection_name = self::DEFAULT_CONNECTION) {
646 $this->_table_name = $table_name;
647 $this->_data = $data;
648
649 $this->_connection_name = $connection_name;
650 self::_setup_db_config($connection_name);
651 }
652
653 /**
654 * Create a new, empty instance of the class. Used
655 * to add a new row to your database. May optionally
656 * be passed an associative array of data to populate
657 * the instance. If so, all fields will be flagged as
658 * dirty so all will be saved to the database when
659 * save() is called.
660 */
661 public function create($data=null) {
662 $this->_is_new = true;
663 if (!is_null($data)) {
664 return $this->hydrate($data)->force_all_dirty();
665 }
666 return $this;
667 }
668
669 /**
670 * Specify the ID column to use for this instance or array of instances only.
671 * This overrides the id_column and id_column_overrides settings.
672 *
673 * This is mostly useful for libraries built on top of Idiorm, and will
674 * not normally be used in manually built queries. If you don't know why
675 * you would want to use this, you should probably just ignore it.
676 */
677 public function use_id_column($id_column) {
678 $this->_instance_id_column = $id_column;
679 return $this;
680 }
681
682 /**
683 * Create an ORM instance from the given row (an associative
684 * array of data fetched from the database)
685 */
686 protected function _create_instance_from_row($row) {
687 $instance = self::for_table($this->_table_name, $this->_connection_name);
688 $instance->use_id_column($this->_instance_id_column);
689 $instance->hydrate($row);
690 return $instance;
691 }
692
693 /**
694 * Tell the ORM that you are expecting a single result
695 * back from your query, and execute it. Will return
696 * a single instance of the ORM class, or false if no
697 * rows were returned.
698 * As a shortcut, you may supply an ID as a parameter
699 * to this method. This will perform a primary key
700 * lookup on the table.
701 */
702 public function find_one($id=null) {
703 if (!is_null($id)) {
704 $this->where_id_is($id);
705 }
706 $this->limit(1);
707 $rows = $this->_run();
708
709 if (empty($rows)) {
710 return false;
711 }
712
713 return $this->_create_instance_from_row($rows[0]);
714 }
715
716 /**
717 * Tell the ORM that you are expecting multiple results
718 * from your query, and execute it. Will return an array
719 * of instances of the ORM class, or an empty array if
720 * no rows were returned.
721 * @return array|\IdiormResultSet
722 */
723 public function find_many() {
724 if(self::$_config[$this->_connection_name]['return_result_sets']) {
725 return $this->find_result_set();
726 }
727 return $this->_find_many();
728 }
729
730 /**
731 * Tell the ORM that you are expecting multiple results
732 * from your query, and execute it. Will return an array
733 * of instances of the ORM class, or an empty array if
734 * no rows were returned.
735 * @return array
736 */
737 protected function _find_many() {
738 $rows = $this->_run();
739 return array_map(array($this, '_create_instance_from_row'), $rows);
740 }
741
742 /**
743 * Tell the ORM that you are expecting multiple results
744 * from your query, and execute it. Will return a result set object
745 * containing instances of the ORM class.
746 * @return \IdiormResultSet
747 */
748 public function find_result_set() {
749 return new IdiormResultSet($this->_find_many());
750 }
751
752 /**
753 * Tell the ORM that you are expecting multiple results
754 * from your query, and execute it. Will return an array,
755 * or an empty array if no rows were returned.
756 * @return array
757 */
758 public function find_array() {
759 return $this->_run();
760 }
761
762 /**
763 * Tell the ORM that you wish to execute a COUNT query.
764 * Will return an integer representing the number of
765 * rows returned.
766 */
767 public function count($column = '*') {
768 return $this->_call_aggregate_db_function(__FUNCTION__, $column);
769 }
770
771 /**
772 * Tell the ORM that you wish to execute a MAX query.
773 * Will return the max value of the choosen column.
774 */
775 public function max($column) {
776 return $this->_call_aggregate_db_function(__FUNCTION__, $column);
777 }
778
779 /**
780 * Tell the ORM that you wish to execute a MIN query.
781 * Will return the min value of the choosen column.
782 */
783 public function min($column) {
784 return $this->_call_aggregate_db_function(__FUNCTION__, $column);
785 }
786
787 /**
788 * Tell the ORM that you wish to execute a AVG query.
789 * Will return the average value of the choosen column.
790 */
791 public function avg($column) {
792 return $this->_call_aggregate_db_function(__FUNCTION__, $column);
793 }
794
795 /**
796 * Tell the ORM that you wish to execute a SUM query.
797 * Will return the sum of the choosen column.
798 */
799 public function sum($column) {
800 return $this->_call_aggregate_db_function(__FUNCTION__, $column);
801 }
802
803 /**
804 * Execute an aggregate query on the current connection.
805 * @param string $sql_function The aggregate function to call eg. MIN, COUNT, etc
806 * @param string $column The column to execute the aggregate query against
807 * @return int
808 */
809 protected function _call_aggregate_db_function($sql_function, $column) {
810 $alias = strtolower($sql_function);
811 $sql_function = strtoupper($sql_function);
812 if('*' != $column) {
813 $column = $this->_quote_identifier($column);
814 }
815 $result_columns = $this->_result_columns;
816 $this->_result_columns = array();
817 $this->select_expr("$sql_function($column)", $alias);
818 $result = $this->find_one();
819 $this->_result_columns = $result_columns;
820
821 $return_value = 0;
822 if($result !== false && isset($result->$alias)) {
823 if (!is_numeric($result->$alias)) {
824 $return_value = $result->$alias;
825 }
826 elseif((int) $result->$alias == (float) $result->$alias) {
827 $return_value = (int) $result->$alias;
828 } else {
829 $return_value = (float) $result->$alias;
830 }
831 }
832 return $return_value;
833 }
834
835 /**
836 * This method can be called to hydrate (populate) this
837 * instance of the class from an associative array of data.
838 * This will usually be called only from inside the class,
839 * but it's public in case you need to call it directly.
840 */
841 public function hydrate($data=array()) {
842 $this->_data = $data;
843 return $this;
844 }
845
846 /**
847 * Force the ORM to flag all the fields in the $data array
848 * as "dirty" and therefore update them when save() is called.
849 */
850 public function force_all_dirty() {
851 $this->_dirty_fields = $this->_data;
852 return $this;
853 }
854
855 /**
856 * Perform a raw query. The query can contain placeholders in
857 * either named or question mark style. If placeholders are
858 * used, the parameters should be an array of values which will
859 * be bound to the placeholders in the query. If this method
860 * is called, all other query building methods will be ignored.
861 */
862 public function raw_query($query, $parameters = array()) {
863 $this->_is_raw_query = true;
864 $this->_raw_query = $query;
865 $this->_raw_parameters = $parameters;
866 return $this;
867 }
868
869 /**
870 * Add an alias for the main table to be used in SELECT queries
871 */
872 public function table_alias($alias) {
873 $this->_table_alias = $alias;
874 return $this;
875 }
876
877 /**
878 * Internal method to add an unquoted expression to the set
879 * of columns returned by the SELECT query. The second optional
880 * argument is the alias to return the expression as.
881 */
882 protected function _add_result_column($expr, $alias=null) {
883 if (!is_null($alias)) {
884 $expr .= " AS " . $this->_quote_identifier($alias);
885 }
886
887 if ($this->_using_default_result_columns) {
888 $this->_result_columns = array($expr);
889 $this->_using_default_result_columns = false;
890 } else {
891 $this->_result_columns[] = $expr;
892 }
893 return $this;
894 }
895
896 /**
897 * Counts the number of columns that belong to the primary
898 * key and their value is null.
899 */
900 public function count_null_id_columns() {
901 if (is_array($this->_get_id_column_name())) {
902 return count(array_filter($this->id(), 'is_null'));
903 } else {
904 return is_null($this->id()) ? 1 : 0;
905 }
906 }
907
908 /**
909 * Add a column to the list of columns returned by the SELECT
910 * query. This defaults to '*'. The second optional argument is
911 * the alias to return the column as.
912 */
913 public function select($column, $alias=null) {
914 $column = $this->_quote_identifier($column);
915 return $this->_add_result_column($column, $alias);
916 }
917
918 /**
919 * Add an unquoted expression to the list of columns returned
920 * by the SELECT query. The second optional argument is
921 * the alias to return the column as.
922 */
923 public function select_expr($expr, $alias=null) {
924 return $this->_add_result_column($expr, $alias);
925 }
926
927 /**
928 * Add columns to the list of columns returned by the SELECT
929 * query. This defaults to '*'. Many columns can be supplied
930 * as either an array or as a list of parameters to the method.
931 *
932 * Note that the alias must not be numeric - if you want a
933 * numeric alias then prepend it with some alpha chars. eg. a1
934 *
935 * @example select_many(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5');
936 * @example select_many('column', 'column2', 'column3');
937 * @example select_many(array('column', 'column2', 'column3'), 'column4', 'column5');
938 *
939 * @return ORM
940 */
941 public function select_many() {
942 $columns = func_get_args();
943 if(!empty($columns)) {
944 $columns = $this->_normalise_select_many_columns($columns);
945 foreach($columns as $alias => $column) {
946 if(is_numeric($alias)) {
947 $alias = null;
948 }
949 $this->select($column, $alias);
950 }
951 }
952 return $this;
953 }
954
955 /**
956 * Add an unquoted expression to the list of columns returned
957 * by the SELECT query. Many columns can be supplied as either
958 * an array or as a list of parameters to the method.
959 *
960 * Note that the alias must not be numeric - if you want a
961 * numeric alias then prepend it with some alpha chars. eg. a1
962 *
963 * @example select_many_expr(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5')
964 * @example select_many_expr('column', 'column2', 'column3')
965 * @example select_many_expr(array('column', 'column2', 'column3'), 'column4', 'column5')
966 *
967 * @return ORM
968 */
969 public function select_many_expr() {
970 $columns = func_get_args();
971 if(!empty($columns)) {
972 $columns = $this->_normalise_select_many_columns($columns);
973 foreach($columns as $alias => $column) {
974 if(is_numeric($alias)) {
975 $alias = null;
976 }
977 $this->select_expr($column, $alias);
978 }
979 }
980 return $this;
981 }
982
983 /**
984 * Take a column specification for the select many methods and convert it
985 * into a normalised array of columns and aliases.
986 *
987 * It is designed to turn the following styles into a normalised array:
988 *
989 * array(array('alias' => 'column', 'column2', 'alias2' => 'column3'), 'column4', 'column5'))
990 *
991 * @param array $columns
992 * @return array
993 */
994 protected function _normalise_select_many_columns($columns) {
995 $return = array();
996 foreach($columns as $column) {
997 if(is_array($column)) {
998 foreach($column as $key => $value) {
999 if(!is_numeric($key)) {
1000 $return[$key] = $value;
1001 } else {
1002 $return[] = $value;
1003 }
1004 }
1005 } else {
1006 $return[] = $column;
1007 }
1008 }
1009 return $return;
1010 }
1011
1012 /**
1013 * Add a DISTINCT keyword before the list of columns in the SELECT query
1014 */
1015 public function distinct() {
1016 $this->_distinct = true;
1017 return $this;
1018 }
1019
1020 /**
1021 * Internal method to add a JOIN source to the query.
1022 *
1023 * The join_operator should be one of INNER, LEFT OUTER, CROSS etc - this
1024 * will be prepended to JOIN.
1025 *
1026 * The table should be the name of the table to join to.
1027 *
1028 * The constraint may be either a string or an array with three elements. If it
1029 * is a string, it will be compiled into the query as-is, with no escaping. The
1030 * recommended way to supply the constraint is as an array with three elements:
1031 *
1032 * first_column, operator, second_column
1033 *
1034 * Example: array('user.id', '=', 'profile.user_id')
1035 *
1036 * will compile to
1037 *
1038 * ON `user`.`id` = `profile`.`user_id`
1039 *
1040 * The final (optional) argument specifies an alias for the joined table.
1041 */
1042 protected function _add_join_source($join_operator, $table, $constraint, $table_alias=null) {
1043
1044 $join_operator = trim("{$join_operator} JOIN");
1045
1046 $table = $this->_quote_identifier($table);
1047
1048 // Add table alias if present
1049 if (!is_null($table_alias)) {
1050 $table_alias = $this->_quote_identifier($table_alias);
1051 $table .= " {$table_alias}";
1052 }
1053
1054 // Build the constraint
1055 if (is_array($constraint)) {
1056 list($first_column, $operator, $second_column) = $constraint;
1057 $first_column = $this->_quote_identifier($first_column);
1058 $second_column = $this->_quote_identifier($second_column);
1059 $constraint = "{$first_column} {$operator} {$second_column}";
1060 }
1061
1062 $this->_join_sources[] = "{$join_operator} {$table} ON {$constraint}";
1063 return $this;
1064 }
1065
1066 /**
1067 * Add a RAW JOIN source to the query
1068 */
1069 public function raw_join($table, $constraint, $table_alias, $parameters = array()) {
1070 // Add table alias if present
1071 if (!is_null($table_alias)) {
1072 $table_alias = $this->_quote_identifier($table_alias);
1073 $table .= " {$table_alias}";
1074 }
1075
1076 $this->_values = array_merge($this->_values, $parameters);
1077
1078 // Build the constraint
1079 if (is_array($constraint)) {
1080 list($first_column, $operator, $second_column) = $constraint;
1081 $first_column = $this->_quote_identifier($first_column);
1082 $second_column = $this->_quote_identifier($second_column);
1083 $constraint = "{$first_column} {$operator} {$second_column}";
1084 }
1085
1086 $this->_join_sources[] = "{$table} ON {$constraint}";
1087 return $this;
1088 }
1089
1090 /**
1091 * Add a simple JOIN source to the query
1092 */
1093 public function join($table, $constraint, $table_alias=null) {
1094 return $this->_add_join_source("", $table, $constraint, $table_alias);
1095 }
1096
1097 /**
1098 * Add an INNER JOIN souce to the query
1099 */
1100 public function inner_join($table, $constraint, $table_alias=null) {
1101 return $this->_add_join_source("INNER", $table, $constraint, $table_alias);
1102 }
1103
1104 /**
1105 * Add a LEFT OUTER JOIN souce to the query
1106 */
1107 public function left_outer_join($table, $constraint, $table_alias=null) {
1108 return $this->_add_join_source("LEFT OUTER", $table, $constraint, $table_alias);
1109 }
1110
1111 /**
1112 * Add an RIGHT OUTER JOIN souce to the query
1113 */
1114 public function right_outer_join($table, $constraint, $table_alias=null) {
1115 return $this->_add_join_source("RIGHT OUTER", $table, $constraint, $table_alias);
1116 }
1117
1118 /**
1119 * Add an FULL OUTER JOIN souce to the query
1120 */
1121 public function full_outer_join($table, $constraint, $table_alias=null) {
1122 return $this->_add_join_source("FULL OUTER", $table, $constraint, $table_alias);
1123 }
1124
1125 /**
1126 * Internal method to add a HAVING condition to the query
1127 */
1128 protected function _add_having($fragment, $values=array()) {
1129 return $this->_add_condition('having', $fragment, $values);
1130 }
1131
1132 /**
1133 * Internal method to add a HAVING condition to the query
1134 */
1135 protected function _add_simple_having($column_name, $separator, $value) {
1136 return $this->_add_simple_condition('having', $column_name, $separator, $value);
1137 }
1138
1139 /**
1140 * Internal method to add a HAVING clause with multiple values (like IN and NOT IN)
1141 */
1142 public function _add_having_placeholder($column_name, $separator, $values) {
1143 if (!is_array($column_name)) {
1144 $data = array($column_name => $values);
1145 } else {
1146 $data = $column_name;
1147 }
1148 $result = $this;
1149 foreach ($data as $key => $val) {
1150 $column = $result->_quote_identifier($key);
1151 $placeholders = $result->_create_placeholders($val);
1152 $result = $result->_add_having("{$column} {$separator} ({$placeholders})", $val);
1153 }
1154 return $result;
1155 }
1156
1157 /**
1158 * Internal method to add a HAVING clause with no parameters(like IS NULL and IS NOT NULL)
1159 */
1160 public function _add_having_no_value($column_name, $operator) {
1161 $conditions = (is_array($column_name)) ? $column_name : array($column_name);
1162 $result = $this;
1163 foreach($conditions as $column) {
1164 $column = $this->_quote_identifier($column);
1165 $result = $result->_add_having("{$column} {$operator}");
1166 }
1167 return $result;
1168 }
1169
1170 /**
1171 * Internal method to add a WHERE condition to the query
1172 */
1173 protected function _add_where($fragment, $values=array()) {
1174 return $this->_add_condition('where', $fragment, $values);
1175 }
1176
1177 /**
1178 * Internal method to add a WHERE condition to the query
1179 */
1180 protected function _add_simple_where($column_name, $separator, $value) {
1181 return $this->_add_simple_condition('where', $column_name, $separator, $value);
1182 }
1183
1184 /**
1185 * Add a WHERE clause with multiple values (like IN and NOT IN)
1186 */
1187 public function _add_where_placeholder($column_name, $separator, $values) {
1188 if (!is_array($column_name)) {
1189 $data = array($column_name => $values);
1190 } else {
1191 $data = $column_name;
1192 }
1193 $result = $this;
1194 foreach ($data as $key => $val) {
1195 $column = $result->_quote_identifier($key);
1196 $placeholders = $result->_create_placeholders($val);
1197 $result = $result->_add_where("{$column} {$separator} ({$placeholders})", $val);
1198 }
1199 return $result;
1200 }
1201
1202 /**
1203 * Add a WHERE clause with no parameters(like IS NULL and IS NOT NULL)
1204 */
1205 public function _add_where_no_value($column_name, $operator) {
1206 $conditions = (is_array($column_name)) ? $column_name : array($column_name);
1207 $result = $this;
1208 foreach($conditions as $column) {
1209 $column = $this->_quote_identifier($column);
1210 $result = $result->_add_where("{$column} {$operator}");
1211 }
1212 return $result;
1213 }
1214
1215 /**
1216 * Internal method to add a HAVING or WHERE condition to the query
1217 */
1218 protected function _add_condition($type, $fragment, $values=array()) {
1219 $conditions_class_property_name = "_{$type}_conditions";
1220 if (!is_array($values)) {
1221 $values = array($values);
1222 }
1223 array_push($this->$conditions_class_property_name, array(
1224 self::CONDITION_FRAGMENT => $fragment,
1225 self::CONDITION_VALUES => $values,
1226 ));
1227 return $this;
1228 }
1229
1230 /**
1231 * Helper method to compile a simple COLUMN SEPARATOR VALUE
1232 * style HAVING or WHERE condition into a string and value ready to
1233 * be passed to the _add_condition method. Avoids duplication
1234 * of the call to _quote_identifier
1235 *
1236 * If column_name is an associative array, it will add a condition for each column
1237 */
1238 protected function _add_simple_condition($type, $column_name, $separator, $value) {
1239 $multiple = is_array($column_name) ? $column_name : array($column_name => $value);
1240 $result = $this;
1241
1242 foreach($multiple as $key => $val) {
1243 // Add the table name in case of ambiguous columns
1244 if (count($result->_join_sources) > 0 && strpos($key, '.') === false) {
1245 $table = $result->_table_name;
1246 if (!is_null($result->_table_alias)) {
1247 $table = $result->_table_alias;
1248 }
1249
1250 $key = "{$table}.{$key}";
1251 }
1252 $key = $result->_quote_identifier($key);
1253 $result = $result->_add_condition($type, "{$key} {$separator} ?", $val);
1254 }
1255 return $result;
1256 }
1257
1258 /**
1259 * Return a string containing the given number of question marks,
1260 * separated by commas. Eg "?, ?, ?"
1261 */
1262 protected function _create_placeholders($fields) {
1263 if(!empty($fields)) {
1264 $db_fields = array();
1265 foreach($fields as $key => $value) {
1266 // Process expression fields directly into the query
1267 if(array_key_exists($key, $this->_expr_fields)) {
1268 $db_fields[] = $value;
1269 } else {
1270 $db_fields[] = '?';
1271 }
1272 }
1273 return implode(', ', $db_fields);
1274 }
1275 }
1276
1277 /**
1278 * Helper method that filters a column/value array returning only those
1279 * columns that belong to a compound primary key.
1280 *
1281 * If the key contains a column that does not exist in the given array,
1282 * a null value will be returned for it.
1283 */
1284 protected function _get_compound_id_column_values($value) {
1285 $filtered = array();
1286 foreach($this->_get_id_column_name() as $key) {
1287 $filtered[$key] = isset($value[$key]) ? $value[$key] : null;
1288 }
1289 return $filtered;
1290 }
1291
1292 /**
1293 * Helper method that filters an array containing compound column/value
1294 * arrays.
1295 */
1296 protected function _get_compound_id_column_values_array($values) {
1297 $filtered = array();
1298 foreach($values as $value) {
1299 $filtered[] = $this->_get_compound_id_column_values($value);
1300 }
1301 return $filtered;
1302 }
1303
1304 /**
1305 * Add a WHERE column = value clause to your query. Each time
1306 * this is called in the chain, an additional WHERE will be
1307 * added, and these will be ANDed together when the final query
1308 * is built.
1309 *
1310 * If you use an array in $column_name, a new clause will be
1311 * added for each element. In this case, $value is ignored.
1312 */
1313 public function where($column_name, $value=null) {
1314 return $this->where_equal($column_name, $value);
1315 }
1316
1317 /**
1318 * More explicitly named version of for the where() method.
1319 * Can be used if preferred.
1320 */
1321 public function where_equal($column_name, $value=null) {
1322 return $this->_add_simple_where($column_name, '=', $value);
1323 }
1324
1325 /**
1326 * Add a WHERE column != value clause to your query.
1327 */
1328 public function where_not_equal($column_name, $value=null) {
1329 return $this->_add_simple_where($column_name, '!=', $value);
1330 }
1331
1332 /**
1333 * Special method to query the table by its primary key
1334 *
1335 * If primary key is compound, only the columns that
1336 * belong to they key will be used for the query
1337 */
1338 public function where_id_is($id) {
1339 return (is_array($this->_get_id_column_name())) ?
1340 $this->where($this->_get_compound_id_column_values($id), null) :
1341 $this->where($this->_get_id_column_name(), $id);
1342 }
1343
1344 /**
1345 * Allows adding a WHERE clause that matches any of the conditions
1346 * specified in the array. Each element in the associative array will
1347 * be a different condition, where the key will be the column name.
1348 *
1349 * By default, an equal operator will be used against all columns, but
1350 * it can be overriden for any or every column using the second parameter.
1351 *
1352 * Each condition will be ORed together when added to the final query.
1353 */
1354 public function where_any_is($values, $operator='=') {
1355 $data = array();
1356 $query = array("((");
1357 $first = true;
1358 foreach ($values as $value) {
1359 if ($first) {
1360 $first = false;
1361 } else {
1362 $query[] = ") OR (";
1363 }
1364 $firstsub = true;
1365 foreach($value as $key => $item) {
1366 $op = is_string($operator) ? $operator : (isset($operator[$key]) ? $operator[$key] : '=');
1367 if ($firstsub) {
1368 $firstsub = false;
1369 } else {
1370 $query[] = "AND";
1371 }
1372 $query[] = $this->_quote_identifier($key);
1373 $data[] = $item;
1374 $query[] = $op . " ?";
1375 }
1376 }
1377 $query[] = "))";
1378 return $this->where_raw(implode(' ', $query), $data);
1379 }
1380
1381 /**
1382 * Similar to where_id_is() but allowing multiple primary keys.
1383 *
1384 * If primary key is compound, only the columns that
1385 * belong to they key will be used for the query
1386 */
1387 public function where_id_in($ids) {
1388 return (is_array($this->_get_id_column_name())) ?
1389 $this->where_any_is($this->_get_compound_id_column_values_array($ids)) :
1390 $this->where_in($this->_get_id_column_name(), $ids);
1391 }
1392
1393 /**
1394 * Add a WHERE ... LIKE clause to your query.
1395 */
1396 public function where_like($column_name, $value=null) {
1397 return $this->_add_simple_where($column_name, 'LIKE', $value);
1398 }
1399
1400 /**
1401 * Add where WHERE ... NOT LIKE clause to your query.
1402 */
1403 public function where_not_like($column_name, $value=null) {
1404 return $this->_add_simple_where($column_name, 'NOT LIKE', $value);
1405 }
1406
1407 /**
1408 * Add a WHERE ... > clause to your query
1409 */
1410 public function where_gt($column_name, $value=null) {
1411 return $this->_add_simple_where($column_name, '>', $value);
1412 }
1413
1414 /**
1415 * Add a WHERE ... < clause to your query
1416 */
1417 public function where_lt($column_name, $value=null) {
1418 return $this->_add_simple_where($column_name, '<', $value);
1419 }
1420
1421 /**
1422 * Add a WHERE ... >= clause to your query
1423 */
1424 public function where_gte($column_name, $value=null) {
1425 return $this->_add_simple_where($column_name, '>=', $value);
1426 }
1427
1428 /**
1429 * Add a WHERE ... <= clause to your query
1430 */
1431 public function where_lte($column_name, $value=null) {
1432 return $this->_add_simple_where($column_name, '<=', $value);
1433 }
1434
1435 /**
1436 * Add a WHERE ... IN clause to your query
1437 */
1438 public function where_in($column_name, $values) {
1439 return $this->_add_where_placeholder($column_name, 'IN', $values);
1440 }
1441
1442 /**
1443 * Add a WHERE ... NOT IN clause to your query
1444 */
1445 public function where_not_in($column_name, $values) {
1446 return $this->_add_where_placeholder($column_name, 'NOT IN', $values);
1447 }
1448
1449 /**
1450 * Add a WHERE column IS NULL clause to your query
1451 */
1452 public function where_null($column_name) {
1453 return $this->_add_where_no_value($column_name, "IS NULL");
1454 }
1455
1456 /**
1457 * Add a WHERE column IS NOT NULL clause to your query
1458 */
1459 public function where_not_null($column_name) {
1460 return $this->_add_where_no_value($column_name, "IS NOT NULL");
1461 }
1462
1463 /**
1464 * Add a raw WHERE clause to the query. The clause should
1465 * contain question mark placeholders, which will be bound
1466 * to the parameters supplied in the second argument.
1467 */
1468 public function where_raw($clause, $parameters=array()) {
1469 return $this->_add_where($clause, $parameters);
1470 }
1471
1472 /**
1473 * Add a LIMIT to the query
1474 */
1475 public function limit($limit) {
1476 $this->_limit = $limit;
1477 return $this;
1478 }
1479
1480 /**
1481 * Add an OFFSET to the query
1482 */
1483 public function offset($offset) {
1484 $this->_offset = $offset;
1485 return $this;
1486 }
1487
1488 /**
1489 * Add an ORDER BY clause to the query
1490 */
1491 protected function _add_order_by($column_name, $ordering) {
1492 $column_name = $this->_quote_identifier($column_name);
1493 $this->_order_by[] = "{$column_name} {$ordering}";
1494 return $this;
1495 }
1496
1497 /**
1498 * Add an ORDER BY column DESC clause
1499 */
1500 public function order_by_desc($column_name) {
1501 return $this->_add_order_by($column_name, 'DESC');
1502 }
1503
1504 /**
1505 * Add an ORDER BY column ASC clause
1506 */
1507 public function order_by_asc($column_name) {
1508 return $this->_add_order_by($column_name, 'ASC');
1509 }
1510
1511 /**
1512 * Add an unquoted expression as an ORDER BY clause
1513 */
1514 public function order_by_expr($clause) {
1515 $this->_order_by[] = $clause;
1516 return $this;
1517 }
1518
1519 /**
1520 * Add a column to the list of columns to GROUP BY
1521 */
1522 public function group_by($column_name) {
1523 $column_name = $this->_quote_identifier($column_name);
1524 $this->_group_by[] = $column_name;
1525 return $this;
1526 }
1527
1528 /**
1529 * Add an unquoted expression to the list of columns to GROUP BY
1530 */
1531 public function group_by_expr($expr) {
1532 $this->_group_by[] = $expr;
1533 return $this;
1534 }
1535
1536 /**
1537 * Add a HAVING column = value clause to your query. Each time
1538 * this is called in the chain, an additional HAVING will be
1539 * added, and these will be ANDed together when the final query
1540 * is built.
1541 *
1542 * If you use an array in $column_name, a new clause will be
1543 * added for each element. In this case, $value is ignored.
1544 */
1545 public function having($column_name, $value=null) {
1546 return $this->having_equal($column_name, $value);
1547 }
1548
1549 /**
1550 * More explicitly named version of for the having() method.
1551 * Can be used if preferred.
1552 */
1553 public function having_equal($column_name, $value=null) {
1554 return $this->_add_simple_having($column_name, '=', $value);
1555 }
1556
1557 /**
1558 * Add a HAVING column != value clause to your query.
1559 */
1560 public function having_not_equal($column_name, $value=null) {
1561 return $this->_add_simple_having($column_name, '!=', $value);
1562 }
1563
1564 /**
1565 * Special method to query the table by its primary key.
1566 *
1567 * If primary key is compound, only the columns that
1568 * belong to they key will be used for the query
1569 */
1570 public function having_id_is($id) {
1571 return (is_array($this->_get_id_column_name())) ?
1572 $this->having($this->_get_compound_id_column_values($id), null) :
1573 $this->having($this->_get_id_column_name(), $id);
1574 }
1575
1576 /**
1577 * Add a HAVING ... LIKE clause to your query.
1578 */
1579 public function having_like($column_name, $value=null) {
1580 return $this->_add_simple_having($column_name, 'LIKE', $value);
1581 }
1582
1583 /**
1584 * Add where HAVING ... NOT LIKE clause to your query.
1585 */
1586 public function having_not_like($column_name, $value=null) {
1587 return $this->_add_simple_having($column_name, 'NOT LIKE', $value);
1588 }
1589
1590 /**
1591 * Add a HAVING ... > clause to your query
1592 */
1593 public function having_gt($column_name, $value=null) {
1594 return $this->_add_simple_having($column_name, '>', $value);
1595 }
1596
1597 /**
1598 * Add a HAVING ... < clause to your query
1599 */
1600 public function having_lt($column_name, $value=null) {
1601 return $this->_add_simple_having($column_name, '<', $value);
1602 }
1603
1604 /**
1605 * Add a HAVING ... >= clause to your query
1606 */
1607 public function having_gte($column_name, $value=null) {
1608 return $this->_add_simple_having($column_name, '>=', $value);
1609 }
1610
1611 /**
1612 * Add a HAVING ... <= clause to your query
1613 */
1614 public function having_lte($column_name, $value=null) {
1615 return $this->_add_simple_having($column_name, '<=', $value);
1616 }
1617
1618 /**
1619 * Add a HAVING ... IN clause to your query
1620 */
1621 public function having_in($column_name, $values=null) {
1622 return $this->_add_having_placeholder($column_name, 'IN', $values);
1623 }
1624
1625 /**
1626 * Add a HAVING ... NOT IN clause to your query
1627 */
1628 public function having_not_in($column_name, $values=null) {
1629 return $this->_add_having_placeholder($column_name, 'NOT IN', $values);
1630 }
1631
1632 /**
1633 * Add a HAVING column IS NULL clause to your query
1634 */
1635 public function having_null($column_name) {
1636 return $this->_add_having_no_value($column_name, 'IS NULL');
1637 }
1638
1639 /**
1640 * Add a HAVING column IS NOT NULL clause to your query
1641 */
1642 public function having_not_null($column_name) {
1643 return $this->_add_having_no_value($column_name, 'IS NOT NULL');
1644 }
1645
1646 /**
1647 * Add a raw HAVING clause to the query. The clause should
1648 * contain question mark placeholders, which will be bound
1649 * to the parameters supplied in the second argument.
1650 */
1651 public function having_raw($clause, $parameters=array()) {
1652 return $this->_add_having($clause, $parameters);
1653 }
1654
1655 /**
1656 * Build a SELECT statement based on the clauses that have
1657 * been passed to this instance by chaining method calls.
1658 */
1659 protected function _build_select() {
1660 // If the query is raw, just set the $this->_values to be
1661 // the raw query parameters and return the raw query
1662 if ($this->_is_raw_query) {
1663 $this->_values = $this->_raw_parameters;
1664 return $this->_raw_query;
1665 }
1666
1667 // Build and return the full SELECT statement by concatenating
1668 // the results of calling each separate builder method.
1669 return $this->_join_if_not_empty(" ", array(
1670 $this->_build_select_start(),
1671 $this->_build_join(),
1672 $this->_build_where(),
1673 $this->_build_group_by(),
1674 $this->_build_having(),
1675 $this->_build_order_by(),
1676 $this->_build_limit(),
1677 $this->_build_offset(),
1678 ));
1679 }
1680
1681 /**
1682 * Build the start of the SELECT statement
1683 */
1684 protected function _build_select_start() {
1685 $fragment = 'SELECT ';
1686 $result_columns = implode(', ', $this->_result_columns);
1687
1688 if (!is_null($this->_limit) &&
1689 self::$_config[$this->_connection_name]['limit_clause_style'] === ORM::LIMIT_STYLE_TOP_N) {
1690 $fragment .= "TOP {$this->_limit} ";
1691 }
1692
1693 if ($this->_distinct) {
1694 $result_columns = 'DISTINCT ' . $result_columns;
1695 }
1696
1697 $fragment .= "{$result_columns} FROM " . $this->_quote_identifier($this->_table_name);
1698
1699 if (!is_null($this->_table_alias)) {
1700 $fragment .= " " . $this->_quote_identifier($this->_table_alias);
1701 }
1702 return $fragment;
1703 }
1704
1705 /**
1706 * Build the JOIN sources
1707 */
1708 protected function _build_join() {
1709 if (count($this->_join_sources) === 0) {
1710 return '';
1711 }
1712
1713 return implode(' ', $this->_join_sources);
1714 }
1715
1716 /**
1717 * Build the WHERE clause(s)
1718 */
1719 protected function _build_where() {
1720 return $this->_build_conditions('where');
1721 }
1722
1723 /**
1724 * Build the HAVING clause(s)
1725 */
1726 protected function _build_having() {
1727 return $this->_build_conditions('having');
1728 }
1729
1730 /**
1731 * Build GROUP BY
1732 */
1733 protected function _build_group_by() {
1734 if (count($this->_group_by) === 0) {
1735 return '';
1736 }
1737 return "GROUP BY " . implode(', ', $this->_group_by);
1738 }
1739
1740 /**
1741 * Build a WHERE or HAVING clause
1742 * @param string $type
1743 * @return string
1744 */
1745 protected function _build_conditions($type) {
1746 $conditions_class_property_name = "_{$type}_conditions";
1747 // If there are no clauses, return empty string
1748 if (count($this->$conditions_class_property_name) === 0) {
1749 return '';
1750 }
1751
1752 $conditions = array();
1753 foreach ($this->$conditions_class_property_name as $condition) {
1754 $conditions[] = $condition[self::CONDITION_FRAGMENT];
1755 $this->_values = array_merge($this->_values, $condition[self::CONDITION_VALUES]);
1756 }
1757
1758 return strtoupper($type) . " " . implode(' AND ', $conditions);
1759 }
1760
1761 /**
1762 * Build ORDER BY
1763 */
1764 protected function _build_order_by() {
1765 if (count($this->_order_by) === 0) {
1766 return '';
1767 }
1768 return "ORDER BY " . implode(', ', $this->_order_by);
1769 }
1770
1771 /**
1772 * Build LIMIT
1773 */
1774 protected function _build_limit() {
1775 $fragment = '';
1776 if (!is_null($this->_limit) &&
1777 self::$_config[$this->_connection_name]['limit_clause_style'] == ORM::LIMIT_STYLE_LIMIT) {
1778 if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') {
1779 $fragment = 'ROWS';
1780 } else {
1781 $fragment = 'LIMIT';
1782 }
1783 $fragment .= " {$this->_limit}";
1784 }
1785 return $fragment;
1786 }
1787
1788 /**
1789 * Build OFFSET
1790 */
1791 protected function _build_offset() {
1792 if (!is_null($this->_offset)) {
1793 $clause = 'OFFSET';
1794 if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'firebird') {
1795 $clause = 'TO';
1796 }
1797 return "$clause " . $this->_offset;
1798 }
1799 return '';
1800 }
1801
1802 /**
1803 * Wrapper around PHP's join function which
1804 * only adds the pieces if they are not empty.
1805 */
1806 protected function _join_if_not_empty($glue, $pieces) {
1807 $filtered_pieces = array();
1808 foreach ($pieces as $piece) {
1809 if (is_string($piece)) {
1810 $piece = trim($piece);
1811 }
1812 if (!empty($piece)) {
1813 $filtered_pieces[] = $piece;
1814 }
1815 }
1816 return implode($glue, $filtered_pieces);
1817 }
1818
1819 /**
1820 * Quote a string that is used as an identifier
1821 * (table names, column names etc). This method can
1822 * also deal with dot-separated identifiers eg table.column
1823 */
1824 protected function _quote_one_identifier($identifier) {
1825 $parts = explode('.', $identifier);
1826 $parts = array_map(array($this, '_quote_identifier_part'), $parts);
1827 return implode('.', $parts);
1828 }
1829
1830 /**
1831 * Quote a string that is used as an identifier
1832 * (table names, column names etc) or an array containing
1833 * multiple identifiers. This method can also deal with
1834 * dot-separated identifiers eg table.column
1835 */
1836 protected function _quote_identifier($identifier) {
1837 if (is_array($identifier)) {
1838 $result = array_map(array($this, '_quote_one_identifier'), $identifier);
1839 return implode(', ', $result);
1840 } else {
1841 return $this->_quote_one_identifier($identifier);
1842 }
1843 }
1844
1845 /**
1846 * This method performs the actual quoting of a single
1847 * part of an identifier, using the identifier quote
1848 * character specified in the config (or autodetected).
1849 */
1850 protected function _quote_identifier_part($part) {
1851 if ($part === '*') {
1852 return $part;
1853 }
1854
1855 $quote_character = self::$_config[$this->_connection_name]['identifier_quote_character'];
1856 // double up any identifier quotes to escape them
1857 return $quote_character .
1858 str_replace($quote_character,
1859 $quote_character . $quote_character,
1860 $part
1861 ) . $quote_character;
1862 }
1863
1864 /**
1865 * Create a cache key for the given query and parameters.
1866 */
1867 protected static function _create_cache_key($query, $parameters, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) {
1868 if(isset(self::$_config[$connection_name]['create_cache_key']) and is_callable(self::$_config[$connection_name]['create_cache_key'])){
1869 return call_user_func_array(self::$_config[$connection_name]['create_cache_key'], array($query, $parameters, $table_name, $connection_name));
1870 }
1871 $parameter_string = implode(',', $parameters);
1872 $key = $query . ':' . $parameter_string;
1873 return sha1($key);
1874 }
1875
1876 /**
1877 * Check the query cache for the given cache key. If a value
1878 * is cached for the key, return the value. Otherwise, return false.
1879 */
1880 protected static function _check_query_cache($cache_key, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) {
1881 if(isset(self::$_config[$connection_name]['check_query_cache']) and is_callable(self::$_config[$connection_name]['check_query_cache'])){
1882 return call_user_func_array(self::$_config[$connection_name]['check_query_cache'], array($cache_key, $table_name, $connection_name));
1883 } elseif (isset(self::$_query_cache[$connection_name][$cache_key])) {
1884 return self::$_query_cache[$connection_name][$cache_key];
1885 }
1886 return false;
1887 }
1888
1889 /**
1890 * Clear the query cache
1891 */
1892 public static function clear_cache($table_name = null, $connection_name = self::DEFAULT_CONNECTION) {
1893 self::$_query_cache = array();
1894 if(isset(self::$_config[$connection_name]['clear_cache']) and is_callable(self::$_config[$connection_name]['clear_cache'])){
1895 return call_user_func_array(self::$_config[$connection_name]['clear_cache'], array($table_name, $connection_name));
1896 }
1897 }
1898
1899 /**
1900 * Add the given value to the query cache.
1901 */
1902 protected static function _cache_query_result($cache_key, $value, $table_name = null, $connection_name = self::DEFAULT_CONNECTION) {
1903 if(isset(self::$_config[$connection_name]['cache_query_result']) and is_callable(self::$_config[$connection_name]['cache_query_result'])){
1904 return call_user_func_array(self::$_config[$connection_name]['cache_query_result'], array($cache_key, $value, $table_name, $connection_name));
1905 } elseif (!isset(self::$_query_cache[$connection_name])) {
1906 self::$_query_cache[$connection_name] = array();
1907 }
1908 self::$_query_cache[$connection_name][$cache_key] = $value;
1909 }
1910
1911 /**
1912 * Execute the SELECT query that has been built up by chaining methods
1913 * on this class. Return an array of rows as associative arrays.
1914 */
1915 protected function _run() {
1916 $query = $this->_build_select();
1917 $caching_enabled = self::$_config[$this->_connection_name]['caching'];
1918
1919 if ($caching_enabled) {
1920 $cache_key = self::_create_cache_key($query, $this->_values, $this->_table_name, $this->_connection_name);
1921 $cached_result = self::_check_query_cache($cache_key, $this->_table_name, $this->_connection_name);
1922
1923 if ($cached_result !== false) {
1924 $this->_reset_idiorm_state();
1925 return $cached_result;
1926 }
1927 }
1928
1929 self::_execute($query, $this->_values, $this->_connection_name);
1930 $statement = self::get_last_statement();
1931
1932 $rows = array();
1933 while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
1934 $rows[] = $row;
1935 }
1936
1937 if ($caching_enabled) {
1938 self::_cache_query_result($cache_key, $rows, $this->_table_name, $this->_connection_name);
1939 }
1940
1941 $this->_reset_idiorm_state();
1942 return $rows;
1943 }
1944
1945 /**
1946 * Reset the Idiorm instance state
1947 */
1948 private function _reset_idiorm_state() {
1949 $this->_values = array();
1950 $this->_result_columns = array('*');
1951 $this->_using_default_result_columns = true;
1952 }
1953
1954 /**
1955 * Return the raw data wrapped by this ORM
1956 * instance as an associative array. Column
1957 * names may optionally be supplied as arguments,
1958 * if so, only those keys will be returned.
1959 */
1960 public function as_array() {
1961 if (func_num_args() === 0) {
1962 return $this->_data;
1963 }
1964 $args = func_get_args();
1965 return array_intersect_key($this->_data, array_flip($args));
1966 }
1967
1968 /**
1969 * Return the value of a property of this object (database row)
1970 * or null if not present.
1971 *
1972 * If a column-names array is passed, it will return a associative array
1973 * with the value of each column or null if it is not present.
1974 */
1975 public function get($key) {
1976 if (is_array($key)) {
1977 $result = array();
1978 foreach($key as $column) {
1979 $result[$column] = isset($this->_data[$column]) ? $this->_data[$column] : null;
1980 }
1981 return $result;
1982 } else {
1983 return isset($this->_data[$key]) ? $this->_data[$key] : null;
1984 }
1985 }
1986
1987 /**
1988 * Return the name of the column in the database table which contains
1989 * the primary key ID of the row.
1990 */
1991 protected function _get_id_column_name() {
1992 if (!is_null($this->_instance_id_column)) {
1993 return $this->_instance_id_column;
1994 }
1995 if (isset(self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name])) {
1996 return self::$_config[$this->_connection_name]['id_column_overrides'][$this->_table_name];
1997 }
1998 return self::$_config[$this->_connection_name]['id_column'];
1999 }
2000
2001 /**
2002 * Get the primary key ID of this object.
2003 */
2004 public function id($disallow_null = false) {
2005 $id = $this->get($this->_get_id_column_name());
2006
2007 if ($disallow_null) {
2008 if (is_array($id)) {
2009 foreach ($id as $id_part) {
2010 if ($id_part === null) {
2011 throw new Exception('Primary key ID contains null value(s)');
2012 }
2013 }
2014 } else if ($id === null) {
2015 throw new Exception('Primary key ID missing from row or is null');
2016 }
2017 }
2018
2019 return $id;
2020 }
2021
2022 /**
2023 * Set a property to a particular value on this object.
2024 * To set multiple properties at once, pass an associative array
2025 * as the first parameter and leave out the second parameter.
2026 * Flags the properties as 'dirty' so they will be saved to the
2027 * database when save() is called.
2028 */
2029 public function set($key, $value = null) {
2030 return $this->_set_orm_property($key, $value);
2031 }
2032
2033 /**
2034 * Set a property to a particular value on this object.
2035 * To set multiple properties at once, pass an associative array
2036 * as the first parameter and leave out the second parameter.
2037 * Flags the properties as 'dirty' so they will be saved to the
2038 * database when save() is called.
2039 * @param string|array $key
2040 * @param string|null $value
2041 */
2042 public function set_expr($key, $value = null) {
2043 return $this->_set_orm_property($key, $value, true);
2044 }
2045
2046 /**
2047 * Set a property on the ORM object.
2048 * @param string|array $key
2049 * @param string|null $value
2050 * @param bool $raw Whether this value should be treated as raw or not
2051 */
2052 protected function _set_orm_property($key, $value = null, $expr = false) {
2053 if (!is_array($key)) {
2054 $key = array($key => $value);
2055 }
2056 foreach ($key as $field => $value) {
2057 $this->_data[$field] = $value;
2058 $this->_dirty_fields[$field] = $value;
2059 if (false === $expr and isset($this->_expr_fields[$field])) {
2060 unset($this->_expr_fields[$field]);
2061 } else if (true === $expr) {
2062 $this->_expr_fields[$field] = true;
2063 }
2064 }
2065 return $this;
2066 }
2067
2068 /**
2069 * Check whether the given field has been changed since this
2070 * object was saved.
2071 */
2072 public function is_dirty($key) {
2073 return array_key_exists($key, $this->_dirty_fields);
2074 }
2075
2076 /**
2077 * Check whether the model was the result of a call to create() or not
2078 * @return bool
2079 */
2080 public function is_new() {
2081 return $this->_is_new;
2082 }
2083
2084 /**
2085 * Save any fields which have been modified on this object
2086 * to the database.
2087 */
2088 public function save() {
2089 $query = array();
2090
2091 // remove any expression fields as they are already baked into the query
2092 $values = array_values(array_diff_key($this->_dirty_fields, $this->_expr_fields));
2093
2094 if (!$this->_is_new) { // UPDATE
2095 // If there are no dirty values, do nothing
2096 if (empty($values) && empty($this->_expr_fields)) {
2097 return true;
2098 }
2099 $query = $this->_build_update();
2100 $id = $this->id(true);
2101 if (is_array($id)) {
2102 $values = array_merge($values, array_values($id));
2103 } else {
2104 $values[] = $id;
2105 }
2106 } else { // INSERT
2107 $query = $this->_build_insert();
2108 }
2109
2110 $success = self::_execute($query, $values, $this->_connection_name);
2111 $caching_auto_clear_enabled = self::$_config[$this->_connection_name]['caching_auto_clear'];
2112 if($caching_auto_clear_enabled){
2113 self::clear_cache($this->_table_name, $this->_connection_name);
2114 }
2115 // If we've just inserted a new record, set the ID of this object
2116 if ($this->_is_new) {
2117 $this->_is_new = false;
2118 if ($this->count_null_id_columns() != 0) {
2119 $db = self::get_db($this->_connection_name);
2120 if($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
2121 // it may return several columns if a compound primary
2122 // key is used
2123 $row = self::get_last_statement()->fetch(PDO::FETCH_ASSOC);
2124 foreach($row as $key => $value) {
2125 $this->_data[$key] = $value;
2126 }
2127 } else {
2128 $column = $this->_get_id_column_name();
2129 // if the primary key is compound, assign the last inserted id
2130 // to the first column
2131 if (is_array($column)) {
2132 $column = reset($column);
2133 }
2134 $this->_data[$column] = $db->lastInsertId();
2135 }
2136 }
2137 }
2138
2139 $this->_dirty_fields = $this->_expr_fields = array();
2140 return $success;
2141 }
2142
2143 /**
2144 * Add a WHERE clause for every column that belongs to the primary key
2145 */
2146 public function _add_id_column_conditions(&$query) {
2147 $query[] = "WHERE";
2148 $keys = is_array($this->_get_id_column_name()) ? $this->_get_id_column_name() : array( $this->_get_id_column_name() );
2149 $first = true;
2150 foreach($keys as $key) {
2151 if ($first) {
2152 $first = false;
2153 }
2154 else {
2155 $query[] = "AND";
2156 }
2157 $query[] = $this->_quote_identifier($key);
2158 $query[] = "= ?";
2159 }
2160 }
2161
2162 /**
2163 * Build an UPDATE query
2164 */
2165 protected function _build_update() {
2166 $query = array();
2167 $query[] = "UPDATE {$this->_quote_identifier($this->_table_name)} SET";
2168
2169 $field_list = array();
2170 foreach ($this->_dirty_fields as $key => $value) {
2171 if(!array_key_exists($key, $this->_expr_fields)) {
2172 $value = '?';
2173 }
2174 $field_list[] = "{$this->_quote_identifier($key)} = $value";
2175 }
2176 $query[] = implode(', ', $field_list);
2177 $this->_add_id_column_conditions($query);
2178 return implode(' ', $query);
2179 }
2180
2181 /**
2182 * Build an INSERT query
2183 */
2184 protected function _build_insert() {
2185 $query[] = "INSERT INTO";
2186 $query[] = $this->_quote_identifier($this->_table_name);
2187 $field_list = array_map(array($this, '_quote_identifier'), array_keys($this->_dirty_fields));
2188 $query[] = "(" . implode(', ', $field_list) . ")";
2189 $query[] = "VALUES";
2190
2191 $placeholders = $this->_create_placeholders($this->_dirty_fields);
2192 $query[] = "({$placeholders})";
2193
2194 if (self::get_db($this->_connection_name)->getAttribute(PDO::ATTR_DRIVER_NAME) == 'pgsql') {
2195 $query[] = 'RETURNING ' . $this->_quote_identifier($this->_get_id_column_name());
2196 }
2197
2198 return implode(' ', $query);
2199 }
2200
2201 /**
2202 * Delete this record from the database
2203 */
2204 public function delete() {
2205 $query = array(
2206 "DELETE FROM",
2207 $this->_quote_identifier($this->_table_name)
2208 );
2209 $this->_add_id_column_conditions($query);
2210 return self::_execute(implode(' ', $query), is_array($this->id(true)) ? array_values($this->id(true)) : array($this->id(true)), $this->_connection_name);
2211 }
2212
2213 /**
2214 * Delete many records from the database
2215 */
2216 public function delete_many() {
2217 // Build and return the full DELETE statement by concatenating
2218 // the results of calling each separate builder method.
2219 $query = $this->_join_if_not_empty(" ", array(
2220 "DELETE FROM",
2221 $this->_quote_identifier($this->_table_name),
2222 $this->_build_where(),
2223 ));
2224
2225 return self::_execute($query, $this->_values, $this->_connection_name);
2226 }
2227
2228 // --------------------- //
2229 // --- ArrayAccess --- //
2230 // --------------------- //
2231
2232 public function offsetExists($key): bool {
2233 return array_key_exists($key, $this->_data);
2234 }
2235
2236 #[\ReturnTypeWillChange] // Need to use annotation since mixed was added in 8.0
2237 public function offsetGet($key) {
2238 return $this->get($key);
2239 }
2240
2241 public function offsetSet($key, $value): void {
2242 if(is_null($key)) {
2243 throw new InvalidArgumentException('You must specify a key/array index.');
2244 }
2245 $this->set($key, $value);
2246 }
2247
2248 public function offsetUnset($key): void {
2249 unset($this->_data[$key]);
2250 unset($this->_dirty_fields[$key]);
2251 }
2252
2253 // --------------------- //
2254 // --- MAGIC METHODS --- //
2255 // --------------------- //
2256 public function __get($key) {
2257 return $this->offsetGet($key);
2258 }
2259
2260 public function __set($key, $value) {
2261 $this->offsetSet($key, $value);
2262 }
2263
2264 public function __unset($key) {
2265 $this->offsetUnset($key);
2266 }
2267
2268
2269 public function __isset($key) {
2270 return $this->offsetExists($key);
2271 }
2272
2273 /**
2274 * Magic method to capture calls to undefined class methods.
2275 * In this case we are attempting to convert camel case formatted
2276 * methods into underscore formatted methods.
2277 *
2278 * This allows us to call ORM methods using camel case and remain
2279 * backwards compatible.
2280 *
2281 * @param string $name
2282 * @param array $arguments
2283 * @return ORM
2284 */
2285 public function __call($name, $arguments)
2286 {
2287 $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name));
2288
2289 if (method_exists($this, $method)) {
2290 return call_user_func_array(array($this, $method), $arguments);
2291 } else {
2292 throw new IdiormMethodMissingException("Method $name() does not exist in class " . get_class($this));
2293 }
2294 }
2295
2296 /**
2297 * Magic method to capture calls to undefined static class methods.
2298 * In this case we are attempting to convert camel case formatted
2299 * methods into underscore formatted methods.
2300 *
2301 * This allows us to call ORM methods using camel case and remain
2302 * backwards compatible.
2303 *
2304 * @param string $name
2305 * @param array $arguments
2306 * @return ORM
2307 */
2308 public static function __callStatic($name, $arguments)
2309 {
2310 $method = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name));
2311
2312 return call_user_func_array(array('MailPoetVendor\Idiorm\ORM', $method), $arguments);
2313 }
2314 }
2315
2316 /**
2317 * A class to handle str_replace operations that involve quoted strings
2318 * @example IdiormString::str_replace_outside_quotes('?', '%s', 'columnA = "Hello?" AND columnB = ?');
2319 * @example IdiormString::value('columnA = "Hello?" AND columnB = ?')->replace_outside_quotes('?', '%s');
2320 * @author Jeff Roberson <ridgerunner@fluxbb.org>
2321 * @author Simon Holywell <treffynnon@php.net>
2322 * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer
2323 */
2324 class IdiormString {
2325 protected $subject;
2326 protected $search;
2327 protected $replace;
2328
2329 /**
2330 * Get an easy to use instance of the class
2331 * @param string $subject
2332 * @return \self
2333 */
2334 public static function value($subject) {
2335 return new self($subject);
2336 }
2337
2338 /**
2339 * Shortcut method: Replace all occurrences of the search string with the replacement
2340 * string where they appear outside quotes.
2341 * @param string $search
2342 * @param string $replace
2343 * @param string $subject
2344 * @return string
2345 */
2346 public static function str_replace_outside_quotes($search, $replace, $subject) {
2347 return self::value($subject)->replace_outside_quotes($search, $replace);
2348 }
2349
2350 /**
2351 * Set the base string object
2352 * @param string $subject
2353 */
2354 public function __construct($subject) {
2355 $this->subject = (string) $subject;
2356 }
2357
2358 /**
2359 * Replace all occurrences of the search string with the replacement
2360 * string where they appear outside quotes
2361 * @param string $search
2362 * @param string $replace
2363 * @return string
2364 */
2365 public function replace_outside_quotes($search, $replace) {
2366 $this->search = $search;
2367 $this->replace = $replace;
2368 return $this->_str_replace_outside_quotes();
2369 }
2370
2371 /**
2372 * Validate an input string and perform a replace on all ocurrences
2373 * of $this->search with $this->replace
2374 * @author Jeff Roberson <ridgerunner@fluxbb.org>
2375 * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer
2376 * @return string
2377 */
2378 protected function _str_replace_outside_quotes(){
2379 $re_valid = '/
2380 # Validate string having embedded quoted substrings.
2381 ^ # Anchor to start of string.
2382 (?: # Zero or more string chunks.
2383 "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # Either a double quoted chunk,
2384 | \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' # or a single quoted chunk,
2385 | [^\'"\\\\]+ # or an unquoted chunk (no escapes).
2386 )* # Zero or more string chunks.
2387 \z # Anchor to end of string.
2388 /sx';
2389 if (!preg_match($re_valid, $this->subject)) {
2390 throw new IdiormStringException("Subject string is not valid in the replace_outside_quotes context.");
2391 }
2392 $re_parse = '/
2393 # Match one chunk of a valid string having embedded quoted substrings.
2394 ( # Either $1: Quoted chunk.
2395 "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" # Either a double quoted chunk,
2396 | \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' # or a single quoted chunk.
2397 ) # End $1: Quoted chunk.
2398 | ([^\'"\\\\]+) # or $2: an unquoted chunk (no escapes).
2399 /sx';
2400 return preg_replace_callback($re_parse, array($this, '_str_replace_outside_quotes_cb'), $this->subject);
2401 }
2402
2403 /**
2404 * Process each matching chunk from preg_replace_callback replacing
2405 * each occurrence of $this->search with $this->replace
2406 * @author Jeff Roberson <ridgerunner@fluxbb.org>
2407 * @link http://stackoverflow.com/a/13370709/461813 StackOverflow answer
2408 * @param array $matches
2409 * @return string
2410 */
2411 protected function _str_replace_outside_quotes_cb($matches) {
2412 // Return quoted string chunks (in group $1) unaltered.
2413 if ($matches[1]) return $matches[1];
2414 // Process only unquoted chunks (in group $2).
2415 return preg_replace('/'. preg_quote($this->search, '/') .'/',
2416 $this->replace, $matches[2]);
2417 }
2418 }
2419
2420 /**
2421 * A result set class for working with collections of model instances
2422 * @author Simon Holywell <treffynnon@php.net>
2423 * @method null setResults(array $results)
2424 * @method array getResults()
2425 */
2426 class IdiormResultSet implements Countable, IteratorAggregate, ArrayAccess {
2427 /**
2428 * The current result set as an array
2429 * @var array
2430 */
2431 protected $_results = array();
2432
2433 /**
2434 * Optionally set the contents of the result set by passing in array
2435 * @param array $results
2436 */
2437 public function __construct(array $results = array()) {
2438 $this->set_results($results);
2439 }
2440
2441 /**
2442 * Set the contents of the result set by passing in array
2443 * @param array $results
2444 */
2445 public function set_results(array $results) {
2446 $this->_results = $results;
2447 }
2448
2449 /**
2450 * Get the current result set as an array
2451 * @return array
2452 */
2453 public function get_results() {
2454 return $this->_results;
2455 }
2456
2457 /**
2458 * Get the current result set as an array
2459 * @return array
2460 */
2461 public function as_array() {
2462 return $this->get_results();
2463 }
2464
2465 /**
2466 * Get the number of records in the result set
2467 * @return int
2468 */
2469 public function count(): int {
2470 return count($this->_results);
2471 }
2472
2473 /**
2474 * Get an iterator for this object. In this case it supports foreaching
2475 * over the result set.
2476 * @return \ArrayIterator
2477 */
2478 public function getIterator(): \Traversable {
2479 return new ArrayIterator($this->_results);
2480 }
2481
2482 /**
2483 * ArrayAccess
2484 * @param int|string $offset
2485 * @return bool
2486 */
2487 public function offsetExists($offset): bool {
2488 return isset($this->_results[$offset]);
2489 }
2490
2491 /**
2492 * ArrayAccess
2493 * @param int|string $offset
2494 * @return mixed
2495 */
2496 #[\ReturnTypeWillChange] // Can't use mixed return typehint since it was added in 8.0
2497 public function offsetGet($offset) {
2498 return $this->_results[$offset];
2499 }
2500
2501 /**
2502 * ArrayAccess
2503 * @param int|string $offset
2504 * @param mixed $value
2505 */
2506 public function offsetSet($offset, $value): void {
2507 $this->_results[$offset] = $value;
2508 }
2509
2510 /**
2511 * ArrayAccess
2512 * @param int|string $offset
2513 */
2514 public function offsetUnset($offset): void {
2515 unset($this->_results[$offset]);
2516 }
2517
2518 /**
2519 * Serializable
2520 * @return string
2521 */
2522 public function serialize() {
2523 return serialize($this->_results);
2524 }
2525
2526 public function __serialize() {
2527 return $this->serialize();
2528 }
2529
2530 /**
2531 * Serializable
2532 * @param string $serialized
2533 * @return array
2534 */
2535 public function unserialize($serialized) {
2536 return unserialize($serialized);
2537 }
2538
2539 public function __unserialize($serialized) {
2540 return $this->unserialize($serialized);
2541 }
2542
2543 /**
2544 * Call a method on all models in a result set. This allows for method
2545 * chaining such as setting a property on all models in a result set or
2546 * any other batch operation across models.
2547 * @example ORM::for_table('Widget')->find_many()->set('field', 'value')->save();
2548 * @param string $method
2549 * @param array $params
2550 * @return IdiormResultSet
2551 */
2552 public function __call($method, $params = array()) {
2553 foreach($this->_results as $model) {
2554 if (method_exists($model, $method)) {
2555 call_user_func_array(array($model, $method), $params);
2556 } else {
2557 throw new IdiormMethodMissingException("Method $method() does not exist in class " . get_class($this));
2558 }
2559 }
2560 return $this;
2561 }
2562 }
2563
2564 /**
2565 * A placeholder for exceptions eminating from the IdiormString class
2566 */
2567 class IdiormStringException extends Exception {}
2568
2569 class IdiormMethodMissingException extends Exception {}
2570