· 7 years ago · Dec 05, 2018, 11:54 PM
1<?php
2/*
3 * Copyright 2010-2012 by MODX, LLC.
4 *
5 * This file is part of xPDO.
6 *
7 * xPDO is free software; you can redistribute it and/or modify it under the
8 * terms of the GNU General Public License as published by the Free Software
9 * Foundation; either version 2 of the License, or (at your option) any later
10 * version.
11 *
12 * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY
13 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along with
17 * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 * Suite 330, Boston, MA 02111-1307 USA
19 */
20
21/**
22 * The base persistent xPDO object classes.
23 *
24 * This file contains the base persistent object classes, which your user-
25 * defined classes will extend when implementing an xPDO object model.
26 *
27 * @package xpdo
28 * @subpackage om
29 */
30
31/**
32 * The base persistent xPDO object class.
33 *
34 * This is the basis for the entire xPDO object model, and can also be used by a
35 * class generator {@link xPDOGenerator}, ultimately allowing custom classes to
36 * be user-defined in a web interface and framework-generated at runtime.
37 *
38 * @abstract This is an abstract class, and is not represented by an actual
39 * table; it simply defines the member variables and functions needed for object
40 * persistence.
41 *
42 * @package xpdo
43 * @subpackage om
44 */
45class xPDOObject {
46 /**
47 * A convenience reference to the xPDO object.
48 * @var xPDO
49 * @access public
50 */
51 public $xpdo= null;
52
53 /**
54 * Name of the data source container the object belongs to.
55 * @var string
56 * @access public
57 */
58 public $container= null;
59
60 /**
61 * Names of the fields in the data table, fully-qualified with a table name.
62 *
63 * NOTE: For use in table joins to qualify fields with the same name.
64 *
65 * @var array
66 * @access public
67 */
68 public $fieldNames= null;
69
70 /**
71 * The actual class name of an instance.
72 * @var string
73 */
74 public $_class= null;
75
76 /**
77 * The package the class is a part of.
78 * @var string
79 */
80 public $_package= null;
81
82 /**
83 * An alias for this instance of the class.
84 * @var string
85 */
86 public $_alias= null;
87
88 /**
89 * The primary key field (or an array of primary key fields) for this object.
90 * @var string|array
91 * @access public
92 */
93 public $_pk= null;
94
95 /**
96 * The php native type of the primary key field.
97 *
98 * NOTE: Will be an array if multiple primary keys are specified for the object.
99 *
100 * @var string|array
101 * @access public
102 */
103 public $_pktype= null;
104
105 /**
106 * Name of the actual table representing this class.
107 * @var string
108 * @access public
109 */
110 public $_table= null;
111
112 /**
113 * An array of meta data for the table.
114 * @var string
115 * @access public
116 */
117 public $_tableMeta= null;
118
119 /**
120 * An array of field names that have been modified.
121 * @var array
122 * @access public
123 */
124 public $_dirty= array ();
125
126 /**
127 * An array of field names that have not been loaded from the source.
128 * @var array
129 * @access public
130 */
131 public $_lazy= array ();
132
133 /**
134 * An array of key-value pairs representing the fields of the instance.
135 * @var array
136 * @access public
137 */
138 public $_fields= array ();
139
140 /**
141 * An array of metadata definitions for each field in the class.
142 * @var array
143 * @access public
144 */
145 public $_fieldMeta= array ();
146
147 /**
148 * An optional array of field aliases.
149 * @var array
150 */
151 public $_fieldAliases= array();
152
153 /**
154 * An array of aggregate foreign key relationships for the class.
155 * @var array
156 * @access public
157 */
158 public $_aggregates= array ();
159
160 /**
161 * An array of composite foreign key relationships for the class.
162 * @var array
163 * @access public
164 */
165 public $_composites= array ();
166
167 /**
168 * An array of object instances related to this object instance.
169 * @var array
170 * @access public
171 */
172 public $_relatedObjects= array ();
173
174 /**
175 * A validator object responsible for this object instance.
176 * @var xPDOValidator
177 * @access public
178 */
179 public $_validator = null;
180
181 /**
182 * An array of validation rules for this object instance.
183 * @var array
184 * @access public
185 */
186 public $_validationRules = array();
187
188 /**
189 * An array of field names that have been already validated.
190 * @var array
191 * @access public
192 */
193 public $_validated= array ();
194
195 /**
196 * Indicates if the validation map has been loaded.
197 * @var boolean
198 * @access public
199 */
200 public $_validationLoaded= false;
201
202 /**
203 * Indicates if the instance is transient (and thus new).
204 * @var boolean
205 * @access public
206 */
207 public $_new= true;
208
209 /**
210 * Indicates the cacheability of the instance.
211 * @var boolean
212 */
213 public $_cacheFlag= true;
214
215 /**
216 * A collection of various options that can be used on the instance.
217 * @var array
218 */
219 public $_options= array();
220
221 /**
222 * Responsible for loading a result set from the database.
223 *
224 * @static
225 * @param xPDO &$xpdo A valid xPDO instance.
226 * @param string $className Name of the class.
227 * @param xPDOCriteria $criteria A valid xPDOCriteria instance.
228 * @return PDOStatement A reference to a PDOStatement representing the
229 * result set.
230 */
231 public static function & _loadRows(& $xpdo, $className, $criteria) {
232 $rows= null;
233 if ($criteria->prepare()) {
234 if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Attempting to execute query using PDO statement object: " . print_r($criteria->sql, true) . print_r($criteria->bindings, true));
235 $tstart= $xpdo->getMicroTime();
236 if (!$criteria->stmt->execute()) {
237 $tend= $xpdo->getMicroTime();
238 $totaltime= $tend - $tstart;
239 $xpdo->queryTime= $xpdo->queryTime + $totaltime;
240 $xpdo->executedQueries= $xpdo->executedQueries + 1;
241 $errorInfo= $criteria->stmt->errorInfo();
242 $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Error ' . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($errorInfo, true));
243 if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
244 if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) {
245 $tstart= $xpdo->getMicroTime();
246 if (!$criteria->stmt->execute()) {
247 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true));
248 }
249 $tend= $xpdo->getMicroTime();
250 $totaltime= $tend - $tstart;
251 $xpdo->queryTime= $xpdo->queryTime + $totaltime;
252 $xpdo->executedQueries= $xpdo->executedQueries + 1;
253 } else {
254 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true));
255 }
256 }
257 }
258 $rows= & $criteria->stmt;
259 } else {
260 $errorInfo = $xpdo->errorInfo();
261 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true));
262 if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $xpdo->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
263 if ($xpdo->getManager() && $xpdo->manager->createObjectContainer($className)) {
264 if (!$criteria->prepare()) {
265 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error preparing statement for query: {$criteria->sql} - " . print_r($errorInfo, true));
266 } else {
267 $tstart= $xpdo->getMicroTime();
268 if (!$criteria->stmt->execute()) {
269 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement: \n" . print_r($criteria->stmt->errorInfo(), true));
270 }
271 $tend= $xpdo->getMicroTime();
272 $totaltime= $tend - $tstart;
273 $xpdo->queryTime= $xpdo->queryTime + $totaltime;
274 $xpdo->executedQueries= $xpdo->executedQueries + 1;
275 }
276 } else {
277 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $xpdo->errorCode() . " attempting to create object container for class {$className}:\n" . print_r($xpdo->errorInfo(), true));
278 }
279 }
280 }
281 return $rows;
282 }
283
284 /**
285 * Loads an instance from an associative array.
286 *
287 * @static
288 * @param xPDO &$xpdo A valid xPDO instance.
289 * @param string $className Name of the class.
290 * @param xPDOQuery|string $criteria A valid xPDOQuery instance or relation alias.
291 * @param array $row The associative array containing the instance data.
292 * @return xPDOObject A new xPDOObject derivative representing a data row.
293 */
294 public static function _loadInstance(& $xpdo, $className, $criteria, $row) {
295 $rowPrefix= '';
296 if (is_object($criteria) && $criteria instanceof xPDOQuery) {
297 $alias = $criteria->getAlias();
298 $actualClass = $criteria->getClass();
299 } elseif (is_string($criteria) && !empty($criteria)) {
300 $alias = $criteria;
301 $actualClass = $className;
302 } else {
303 $alias = $className;
304 $actualClass= $className;
305 }
306 if (isset ($row["{$alias}_class_key"])) {
307 $actualClass= $row["{$alias}_class_key"];
308 $rowPrefix= $alias . '_';
309 } elseif (isset($row["{$className}_class_key"])) {
310 $actualClass= $row["{$className}_class_key"];
311 $rowPrefix= $className . '_';
312 } elseif (isset ($row['class_key'])) {
313 $actualClass= $row['class_key'];
314 }
315 /** @var xPDOObject $instance */
316 $instance= $xpdo->newObject($actualClass);
317 if (is_object($instance) && $instance instanceof xPDOObject) {
318 $pk = $xpdo->getPK($actualClass);
319 if ($pk) {
320 if (is_array($pk)) $pk = reset($pk);
321 if (isset($row["{$alias}_{$pk}"])) {
322 $rowPrefix= $alias . '_';
323 }
324 elseif ($actualClass !== $className && $actualClass !== $alias && isset($row["{$actualClass}_{$pk}"])) {
325 $rowPrefix= $actualClass . '_';
326 }
327 elseif ($className !== $alias && isset($row["{$className}_{$pk}"])) {
328 $rowPrefix= $className . '_';
329 }
330 } elseif (strpos(strtolower(key($row)), strtolower($alias . '_')) === 0) {
331 $rowPrefix= $alias . '_';
332 } elseif (strpos(strtolower(key($row)), strtolower($className . '_')) === 0) {
333 $rowPrefix= $className . '_';
334 }
335 $parentClass = $className;
336 $isSubPackage = strpos($className,'.');
337 if ($isSubPackage !== false) {
338 $parentClass = substr($className,$isSubPackage+1);
339 }
340 if (!$instance instanceof $parentClass) {
341 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Instantiated a derived class {$actualClass} that is not a subclass of the requested class {$className}");
342 }
343 $instance->_lazy= $actualClass !== $className ? array_keys($xpdo->getFieldMeta($actualClass)) : array_keys($instance->_fieldMeta);
344 $instance->fromArray($row, $rowPrefix, true, true);
345 $instance->_dirty= array ();
346 $instance->_new= false;
347 }
348 return $instance;
349 }
350
351 /**
352 * Responsible for loading an instance into a collection.
353 *
354 * @static
355 * @param xPDO &$xpdo A valid xPDO instance.
356 * @param array &$objCollection The collection to load the instance into.
357 * @param string $className Name of the class.
358 * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
359 * @param boolean|integer $cacheFlag Indicates if the objects should be cached and
360 * optionally, by specifying an integer value, for how many seconds.
361 */
362 public static function _loadCollectionInstance(xPDO & $xpdo, array & $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag=true) {
363 $loaded = false;
364 if ($obj= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row)) {
365 if (($cacheKey= $obj->getPrimaryKey()) && !$obj->isLazy()) {
366 if (is_array($cacheKey)) {
367 $pkval= implode('-', $cacheKey);
368 } else {
369 $pkval= $cacheKey;
370 }
371 /* set OPT_CACHE_DB_COLLECTIONS to 2 to cache instances by primary key from collection result sets */
372 if ($xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1) == 2 && $xpdo->_cacheEnabled && $cacheFlag) {
373 if (!$fromCache) {
374 $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
375 $xpdo->toCache($pkCriteria, $obj, $cacheFlag);
376 } else {
377 $obj->_cacheFlag= true;
378 }
379 }
380 $objCollection[$pkval]= $obj;
381 $loaded = true;
382 } else {
383 $objCollection[]= $obj;
384 $loaded = true;
385 }
386 }
387 return $loaded;
388 }
389
390 /**
391 * Load an instance of an xPDOObject or derivative class.
392 *
393 * @static
394 * @param xPDO &$xpdo A valid xPDO instance.
395 * @param string $className Name of the class.
396 * @param mixed $criteria A valid primary key, criteria array, or
397 * xPDOCriteria instance.
398 * @param boolean|integer $cacheFlag Indicates if the objects should be
399 * cached and optionally, by specifying an integer value, for how many
400 * seconds.
401 * @return object|null An instance of the requested class, or null if it
402 * could not be instantiated.
403 */
404 public static function load(xPDO & $xpdo, $className, $criteria, $cacheFlag= true) {
405 $instance= null;
406 $fromCache= false;
407 if ($className= $xpdo->loadClass($className)) {
408 if (!is_object($criteria)) {
409 $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
410 }
411 if (is_object($criteria)) {
412 $criteria = $xpdo->addDerivativeCriteria($className, $criteria);
413 $row= null;
414 if ($xpdo->_cacheEnabled && $criteria->cacheFlag && $cacheFlag) {
415 $row= $xpdo->fromCache($criteria, $className);
416 }
417 if ($row === null || !is_array($row)) {
418 if ($rows= xPDOObject :: _loadRows($xpdo, $className, $criteria)) {
419 $row= $rows->fetch(PDO::FETCH_ASSOC);
420 $rows->closeCursor();
421 }
422 } else {
423 $fromCache= true;
424 }
425 if (!is_array($row)) {
426 if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Fetched empty result set from statement: " . print_r($criteria->sql, true) . " with bindings: " . print_r($criteria->bindings, true));
427 } else {
428 $instance= xPDOObject :: _loadInstance($xpdo, $className, $criteria, $row);
429 if (is_object($instance)) {
430 if (!$fromCache && $cacheFlag && $xpdo->_cacheEnabled) {
431 $xpdo->toCache($criteria, $instance, $cacheFlag);
432 if ($xpdo->getOption(xPDO::OPT_CACHE_DB_OBJECTS_BY_PK) && ($cacheKey= $instance->getPrimaryKey()) && !$instance->isLazy()) {
433 $pkCriteria = $xpdo->newQuery($className, $cacheKey, $cacheFlag);
434 $xpdo->toCache($pkCriteria, $instance, $cacheFlag);
435 }
436 }
437 if ($xpdo->getDebug() === true) $xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Loaded object instance: " . print_r($instance->toArray('', true), true));
438 }
439 }
440 } else {
441 $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'No valid statement could be found in or generated from the given criteria.');
442 }
443 } else {
444 $xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Invalid class specified: ' . $className);
445 }
446 return $instance;
447 }
448
449 /**
450 * Load a collection of xPDOObject instances.
451 *
452 * @static
453 * @param xPDO &$xpdo A valid xPDO instance.
454 * @param string $className Name of the class.
455 * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
456 * @param boolean|integer $cacheFlag Indicates if the objects should be
457 * cached and optionally, by specifying an integer value, for how many
458 * seconds.
459 * @return array An array of xPDOObject instances or an empty array if no instances are loaded.
460 */
461 public static function loadCollection(xPDO & $xpdo, $className, $criteria= null, $cacheFlag= true) {
462 $objCollection= array ();
463 $fromCache = false;
464 if (!$className= $xpdo->loadClass($className)) return $objCollection;
465 $rows= false;
466 $fromCache= false;
467 $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
468 if (!is_object($criteria)) {
469 $criteria= $xpdo->getCriteria($className, $criteria, $cacheFlag);
470 }
471 if (is_object($criteria)) {
472 $criteria = $xpdo->addDerivativeCriteria($className, $criteria);
473 }
474 if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
475 $rows= $xpdo->fromCache($criteria);
476 $fromCache = (is_array($rows) && !empty($rows));
477 }
478 if (!$fromCache && is_object($criteria)) {
479 $rows= xPDOObject :: _loadRows($xpdo, $className, $criteria);
480 }
481 if (is_array ($rows)) {
482 foreach ($rows as $row) {
483 xPDOObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
484 }
485 } elseif (is_object($rows)) {
486 $cacheRows = array();
487 while ($row = $rows->fetch(PDO::FETCH_ASSOC)) {
488 xPDOObject :: _loadCollectionInstance($xpdo, $objCollection, $className, $criteria, $row, $fromCache, $cacheFlag);
489 if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $cacheRows[] = $row;
490 }
491 if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag && !$fromCache) $rows =& $cacheRows;
492 }
493 if (!$fromCache && $xpdo->_cacheEnabled && $collectionCaching > 0 && $cacheFlag && !empty($rows)) {
494 $xpdo->toCache($criteria, $rows, $cacheFlag);
495 }
496 return $objCollection;
497 }
498
499 /**
500 * Load a collection of xPDOObject instances and a graph of related objects.
501 *
502 * @static
503 * @param xPDO &$xpdo A valid xPDO instance.
504 * @param string $className Name of the class.
505 * @param string|array $graph A related object graph in array or JSON
506 * format, e.g. array('relationAlias'=>array('subRelationAlias'=>array()))
507 * or {"relationAlias":{"subRelationAlias":{}}}. Note that the empty arrays
508 * are necessary in order for the relation to be recognized.
509 * @param mixed $criteria A valid primary key, criteria array, or xPDOCriteria instance.
510 * @param boolean|integer $cacheFlag Indicates if the objects should be
511 * cached and optionally, by specifying an integer value, for how many
512 * seconds.
513 * @return array An array of xPDOObject instances or an empty array if no instances are loaded.
514 */
515 public static function loadCollectionGraph(xPDO & $xpdo, $className, $graph, $criteria, $cacheFlag) {
516 $objCollection = array();
517 if ($query= $xpdo->newQuery($className, $criteria, $cacheFlag)) {
518 $query = $xpdo->addDerivativeCriteria($className, $query);
519 $query->bindGraph($graph);
520 $rows = array();
521 $fromCache = false;
522 $collectionCaching = (integer) $xpdo->getOption(xPDO::OPT_CACHE_DB_COLLECTIONS, array(), 1);
523 if ($collectionCaching > 0 && $xpdo->_cacheEnabled && $cacheFlag) {
524 $rows= $xpdo->fromCache($query);
525 $fromCache = !empty($rows);
526 }
527 if (!$fromCache) {
528 if ($query->prepare()) {
529 if ($query->stmt->execute()) {
530 $objCollection= $query->hydrateGraph($query->stmt, $cacheFlag);
531 } else {
532 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error {$query->stmt->errorCode()} executing query: {$query->sql} - " . print_r($query->stmt->errorInfo(), true));
533 }
534 } else {
535 $xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error {$xpdo->errorCode()} preparing statement: {$query->sql} - " . print_r($xpdo->errorInfo(), true));
536 }
537 } elseif (!empty($rows)) {
538 $objCollection= $query->hydrateGraph($rows, $cacheFlag);
539 }
540 }
541 return $objCollection;
542 }
543
544 /**
545 * Get a set of column names from an xPDOObject for use in SQL queries.
546 *
547 * @static
548 * @param xPDO &$xpdo A reference to an initialized xPDO instance.
549 * @param string $className The class name to get columns from.
550 * @param string $tableAlias An optional alias for the table in the query.
551 * @param string $columnPrefix An optional prefix to prepend to each column name.
552 * @param array $columns An optional array of field names to include or exclude
553 * (include is default behavior).
554 * @param boolean $exclude Determines if any specified columns should be included
555 * or excluded from the set of results.
556 * @return string A comma-delimited list of the field names for use in a SELECT clause.
557 */
558 public static function getSelectColumns(xPDO & $xpdo, $className, $tableAlias= '', $columnPrefix= '', $columns= array (), $exclude= false) {
559 $columnarray= array ();
560 $aColumns= $xpdo->getFields($className);
561 if ($aColumns) {
562 if (!empty ($tableAlias)) {
563 $tableAlias= $xpdo->escape($tableAlias);
564 $tableAlias.= '.';
565 }
566 foreach (array_keys($aColumns) as $k) {
567 if ($exclude && in_array($k, $columns)) {
568 continue;
569 }
570 elseif (empty ($columns)) {
571 $columnarray[$k]= "{$tableAlias}" . $xpdo->escape($k);
572 }
573 elseif ($exclude || in_array($k, $columns)) {
574 $columnarray[$k]= "{$tableAlias}" . $xpdo->escape($k);
575 } else {
576 continue;
577 }
578 if (!empty ($columnPrefix)) {
579 $columnarray[$k]= $columnarray[$k] . " AS " . $xpdo->escape("{$columnPrefix}{$k}");
580 }
581 }
582 }
583 return implode(', ', $columnarray);
584 }
585
586 /**
587 * Constructor
588 *
589 * Do not call the constructor directly; see {@link xPDO::newObject()}.
590 *
591 * All derivatives of xPDOObject must redeclare this method, and must call
592 * the parent method explicitly before any additional logic is executed, e.g.
593 *
594 * <code>
595 * public function __construct(xPDO & $xpdo) {
596 * parent :: __construct($xpdo);
597 * // Any additional constructor tasks here
598 * }
599 * </code>
600 *
601 * @access public
602 * @param xPDO &$xpdo A reference to a valid xPDO instance.
603 * @return xPDOObject
604 */
605 public function __construct(xPDO & $xpdo) {
606 $this->xpdo= & $xpdo;
607 $this->container= $xpdo->config['dbname'];
608 $this->_class= get_class($this);
609 $pos= strrpos($this->_class, '_');
610 if ($pos !== false && substr($this->_class, $pos + 1) == $xpdo->config['dbtype']) {
611 $this->_class= substr($this->_class, 0, $pos);
612 }
613 $this->_package= $xpdo->getPackage($this->_class);
614 $this->_alias= $this->_class;
615 $this->_table= $xpdo->getTableName($this->_class);
616 $this->_tableMeta= $xpdo->getTableMeta($this->_class);
617 $this->_fields= $xpdo->getFields($this->_class);
618 $this->_fieldMeta= $xpdo->getFieldMeta($this->_class);
619 $this->_fieldAliases= $xpdo->getFieldAliases($this->_class);
620 $this->_aggregates= $xpdo->getAggregates($this->_class);
621 $this->_composites= $xpdo->getComposites($this->_class);
622 if ($relatedObjs= array_merge($this->_aggregates, $this->_composites)) {
623 foreach ($relatedObjs as $aAlias => $aMeta) {
624 if (!array_key_exists($aAlias, $this->_relatedObjects)) {
625 if ($aMeta['cardinality'] == 'many') {
626 $this->_relatedObjects[$aAlias]= array ();
627 }
628 else {
629 $this->_relatedObjects[$aAlias]= null;
630 }
631 }
632 }
633 }
634 foreach ($this->_fieldAliases as $fieldAlias => $field) {
635 $this->addFieldAlias($field, $fieldAlias);
636 }
637 $this->setDirty();
638 }
639
640 /**
641 * Add an alias as a reference to an actual field of the object.
642 *
643 * @param string $field The field name to create a reference to.
644 * @param string $alias The name of the reference.
645 * @return bool True if the reference is added successfully.
646 */
647 public function addFieldAlias($field, $alias) {
648 $added = false;
649 if (array_key_exists($field, $this->_fields)) {
650 if (!array_key_exists($alias, $this->_fields)) {
651 $this->_fields[$alias] =& $this->_fields[$field];
652 if (!array_key_exists($alias, $this->_fieldAliases)) {
653 $this->_fieldAliases[$alias] = $field;
654 if (!array_key_exists($alias, $this->xpdo->map[$this->_class]['fieldAliases'])) {
655 $this->xpdo->map[$this->_class]['fieldAliases'][$alias]= $field;
656 }
657 }
658 $added = true;
659 } else {
660 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "The alias {$alias} is already in use as a field name in objects of class {$this->_class}", '', __METHOD__, __FILE__, __LINE__);
661 }
662 }
663 return $added;
664 }
665
666 /**
667 * Get an option value for this instance.
668 *
669 * @param string $key The option key to retrieve a value for.
670 * @param array|null $options An optional array to search for a value in first.
671 * @param mixed $default A default value to return if no value is found; null is the default.
672 * @return mixed The value of the option or the provided default if it is not set.
673 */
674 public function getOption($key, $options = null, $default = null) {
675 if (is_array($options) && array_key_exists($key, $options)) {
676 $value= $options[$key];
677 } elseif (array_key_exists($key, $this->_options)) {
678 $value= $this->_options[$key];
679 } else {
680 $value= $this->xpdo->getOption($key, null, $default);
681 }
682 return $value;
683 }
684
685 /**
686 * Set an option value for this instance.
687 *
688 * @param string $key The option key to set a value for.
689 * @param mixed $value A value to assign to the option.
690 */
691 public function setOption($key, $value) {
692 $this->_options[$key]= $value;
693 }
694
695 public function __get($name) {
696 if ($this->getOption(xPDO::OPT_HYDRATE_FIELDS) && array_key_exists($name, $this->_fields)) {
697 return $this->_fields[$name];
698 } elseif ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS)) {
699 if (array_key_exists($name, $this->_composites)) {
700 $fkMeta = $this->_composites[$name];
701 } elseif (array_key_exists($name, $this->_aggregates)) {
702 $fkMeta = $this->_aggregates[$name];
703 } else {
704 return null;
705 }
706 } else {
707 return null;
708 }
709 if ($fkMeta['cardinality'] === 'many') {
710 return $this->getMany($name);
711 } else {
712 return $this->getOne($name);
713 }
714 }
715
716 public function __set($name, $value) {
717 if ($this->getOption(xPDO::OPT_HYDRATE_FIELDS) && array_key_exists($name, $this->_fields)) {
718 return $this->_setRaw($name, $value);
719 } elseif ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS)) {
720 if (array_key_exists($name, $this->_composites)) {
721 $fkMeta = $this->_composites[$name];
722 } elseif (array_key_exists($name, $this->_aggregates)) {
723 $fkMeta = $this->_aggregates[$name];
724 } else {
725 return false;
726 }
727 } else {
728 return false;
729 }
730 if ($fkMeta['cardinality'] === 'many') {
731 return $this->addMany($value, $name);
732 } else {
733 return $this->addOne($value, $name);
734 }
735 }
736
737 public function __isset($name) {
738 return ($this->getOption(xPDO::OPT_HYDRATE_FIELDS) && array_key_exists($name, $this->_fields) && isset($this->_fields[$name]))
739 || ($this->getOption(xPDO::OPT_HYDRATE_RELATED_OBJECTS)
740 && ((array_key_exists($name, $this->_composites) && isset($this->_composites[$name]))
741 || (array_key_exists($name, $this->_aggregates) && isset($this->_aggregates[$name]))));
742 }
743
744 /**
745 * Set a field value by the field key or name.
746 *
747 * @todo Define and implement field validation.
748 *
749 * @param string $k The field key or name.
750 * @param mixed $v The value to set the field to.
751 * @param string|callable $vType A string indicating the format of the
752 * provided value parameter, or a callable function that should be used to
753 * set the field value, overriding the default behavior.
754 * @return boolean Determines whether the value was set successfully and was
755 * determined to be dirty (i.e. different from the previous value).
756 */
757 public function set($k, $v= null, $vType= '') {
758 $set= false;
759 $callback= '';
760 $callable= !empty($vType) && is_callable($vType, false, $callback) ? true : false;
761 $oldValue= null;
762 $k = $this->getField($k);
763 if (is_string($k) && !empty($k)) {
764 if (array_key_exists($k, $this->_fieldMeta)) {
765 $oldValue= $this->_fields[$k];
766 if (isset ($this->_fieldMeta[$k]['index']) && $this->_fieldMeta[$k]['index'] === 'pk' && isset ($this->_fieldMeta[$k]['generated'])) {
767 if (!$this->_fieldMeta[$k]['generated'] === 'callback') {
768 return false;
769 }
770 }
771 if ($callable && $callback) {
772 $set = $callback($k, $v, $this);
773 } else {
774 if (is_string($v) && $this->getOption(xPDO::OPT_ON_SET_STRIPSLASHES)) {
775 $v= stripslashes($v);
776 }
777 if ($oldValue !== $v) {
778 //type validation
779 $phptype= $this->_fieldMeta[$k]['phptype'];
780 $dbtype= $this->_fieldMeta[$k]['dbtype'];
781 $allowNull= isset($this->_fieldMeta[$k]['null']) ? (boolean) $this->_fieldMeta[$k]['null'] : true;
782 if ($v === null) {
783 if ($allowNull) {
784 $this->_fields[$k]= null;
785 $set= true;
786 } else {
787 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "{$this->_class}: Attempt to set NOT NULL field {$k} to NULL");
788 }
789 }
790 else {
791 switch ($phptype) {
792 case 'timestamp' :
793 case 'datetime' :
794 $ts= false;
795 if (preg_match('/int/i', $dbtype)) {
796 if (strtolower($vType) == 'integer' || is_int($v) || $v == '0') {
797 $ts= (integer) $v;
798 } else {
799 $ts= strtotime($v);
800 }
801 if ($ts === false) {
802 $ts= 0;
803 }
804 $this->_fields[$k]= $ts;
805 $set= true;
806 } else {
807 if ($vType == 'utc' || in_array($v, $this->xpdo->driver->_currentTimestamps) || $v === '0000-00-00 00:00:00') {
808 $this->_fields[$k]= (string) $v;
809 $set= true;
810 } else {
811 if (strtolower($vType) == 'integer' || is_int($v)) {
812 $ts= intval($v);
813 } elseif (is_string($v) && !empty($v)) {
814 $ts= strtotime($v);
815 }
816 if ($ts !== false) {
817 $this->_fields[$k]= strftime('%Y-%m-%d %H:%M:%S', $ts);
818 $set= true;
819 }
820 }
821 }
822 break;
823 case 'date' :
824 if (preg_match('/int/i', $dbtype)) {
825 if (strtolower($vType) == 'integer' || is_int($v) || $v == '0') {
826 $ts= (integer) $v;
827 } else {
828 $ts= strtotime($v);
829 }
830 if ($ts === false) {
831 $ts= 0;
832 }
833 $this->_fields[$k]= $ts;
834 $set= true;
835 } else {
836 if ($vType == 'utc' || in_array($v, $this->xpdo->driver->_currentDates) || $v === '0000-00-00') {
837 $this->_fields[$k]= $v;
838 $set= true;
839 } else {
840 if (strtolower($vType) == 'integer' || is_int($v)) {
841 $ts= intval($v);
842 } elseif (is_string($v) && !empty($v)) {
843 $ts= strtotime($v);
844 }
845 $ts= strtotime($v);
846 if ($ts !== false) {
847 $this->_fields[$k]= strftime('%Y-%m-%d', $ts);
848 $set= true;
849 }
850 }
851 }
852 break;
853 case 'boolean' :
854 $this->_fields[$k]= intval($v);
855 $set= true;
856 break;
857 case 'integer' :
858 $this->_fields[$k]= intval($v);
859 $set= true;
860 break;
861 case 'array' :
862 if (is_object($v) && $v instanceof xPDOObject) {
863 $v = $v->toArray();
864 }
865 if (is_array($v)) {
866 $this->_fields[$k]= serialize($v);
867 $set= true;
868 }
869 break;
870 case 'json' :
871 if (is_object($v) && $v instanceof xPDOObject) {
872 $v = $v->toArray();
873 }
874 if (is_string($v)) {
875 $v= $this->xpdo->fromJSON($v, true);
876 }
877 if (is_array($v)) {
878 $this->_fields[$k]= $this->xpdo->toJSON($v);
879 $set= true;
880 }
881 break;
882 default :
883 $this->_fields[$k]= $v;
884 $set= true;
885 }
886 }
887 }
888 }
889 } elseif ($this->getOption(xPDO::OPT_HYDRATE_ADHOC_FIELDS)) {
890 $oldValue= isset($this->_fields[$k]) ? $this->_fields[$k] : null;
891 if ($callable) {
892 $set = $callback($k, $v, $this);
893 } else {
894 $this->_fields[$k]= $v;
895 $set= true;
896 }
897 }
898 if ($set && $oldValue !== $this->_fields[$k]) {
899 $this->setDirty($k);
900 } else {
901 $set= false;
902 }
903 } else {
904 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject - Called set() with an invalid field name: ' . print_r($k, 1));
905 }
906 return $set;
907 }
908
909 /**
910 * Get a field value (or a set of values) by the field key(s) or name(s).
911 *
912 * Warning: do not use the $format parameter if retrieving multiple values of
913 * different types, as the format string will be applied to all types, most
914 * likely with unpredicatable results. Optionally, you can supply an associate
915 * array of format strings with the field key as the key for the format array.
916 *
917 * @param string|array $k A string (or an array of strings) representing the field
918 * key or name.
919 * @param string|array $format An optional variable (or an array of variables) to
920 * format the return value(s).
921 * @param mixed $formatTemplate An additional optional variable that can be used in
922 * formatting the return value(s).
923 * @return mixed The value(s) of the field(s) requested.
924 */
925 public function get($k, $format = null, $formatTemplate= null) {
926 $value= null;
927 if (is_array($k)) {
928 $lazy = array_intersect($k, $this->_lazy);
929 if ($lazy) {
930 $this->_loadFieldData($lazy);
931 }
932 foreach ($k as $key) {
933 if (array_key_exists($key, $this->_fields)) {
934 if (is_array($format) && isset ($format[$key])) {
935 $formatTpl= null;
936 if (is_array ($formatTemplate) && isset ($formatTemplate[$key])) {
937 $formatTpl= $formatTemplate[$key];
938 }
939 $value[$key]= $this->get($key, $format[$key], $formatTpl);
940 } elseif (!empty ($format) && is_string($format)) {
941 $value[$key]= $this->get($key, $format, $formatTemplate);
942 } else {
943 $value[$key]= $this->get($key);
944 }
945 }
946 }
947 } elseif (is_string($k) && !empty($k)) {
948 if (array_key_exists($k, $this->_fields)) {
949 if ($this->isLazy($k)) {
950 $this->_loadFieldData($k);
951 }
952 $dbType= $this->_getDataType($k);
953 $fieldType= $this->_getPHPType($k);
954 $value= $this->_fields[$k];
955 if ($value !== null) {
956 switch ($fieldType) {
957 case 'boolean' :
958 $value= (boolean) $value;
959 break;
960 case 'integer' :
961 $value= intval($value);
962 if (is_string($format) && !empty ($format)) {
963 if (strpos($format, 're:') === 0) {
964 if (!empty ($formatTemplate) && is_string($formatTemplate)) {
965 $value= preg_replace(substr($format, 3), $formatTemplate, $value);
966 }
967 } else {
968 $value= sprintf($format, $value);
969 }
970 }
971 break;
972 case 'float' :
973 $value= (float) $value;
974 if (is_string($format) && !empty ($format)) {
975 if (strpos($format, 're:') === 0) {
976 if (!empty ($formatTemplate) && is_string($formatTemplate)) {
977 $value= preg_replace(substr($format, 3), $formatTemplate, $value);
978 }
979 } else {
980 $value= sprintf($format, $value);
981 }
982 }
983 break;
984 case 'timestamp' :
985 case 'datetime' :
986 if (preg_match('/int/i', $dbType)) {
987 $ts= intval($value);
988 } elseif (in_array($value, $this->xpdo->driver->_currentTimestamps)) {
989 $ts= time();
990 } else {
991 $ts= strtotime($value);
992 }
993 if ($ts !== false && !empty($value)) {
994 if (is_string($format) && !empty ($format)) {
995 if (strpos($format, 're:') === 0) {
996 $value= date('Y-m-d H:M:S', $ts);
997 if (!empty ($formatTemplate) && is_string($formatTemplate)) {
998 $value= preg_replace(substr($format, 3), $formatTemplate, $value);
999 }
1000 } elseif (strpos($format, '%') === false) {
1001 $value= date($format, $ts);
1002 } else {
1003 $value= strftime($format, $ts);
1004 }
1005 } else {
1006 $value= strftime('%Y-%m-%d %H:%M:%S', $ts);
1007 }
1008 }
1009 break;
1010 case 'date' :
1011 if (preg_match('/int/i', $dbType)) {
1012 $ts= intval($value);
1013 } elseif (in_array($value, $this->xpdo->driver->_currentDates)) {
1014 $ts= time();
1015 } else {
1016 $ts= strtotime($value);
1017 }
1018 if ($ts !== false && !empty($value)) {
1019 if (is_string($format) && !empty ($format)) {
1020 if (strpos($format, 're:') === 0) {
1021 $value= strftime('%Y-%m-%d', $ts);
1022 if (!empty ($formatTemplate) && is_string($formatTemplate)) {
1023 $value= preg_replace(substr($format, 3), $formatTemplate, $value);
1024 }
1025 } elseif (strpos($format, '%') === false) {
1026 $value= date($format, $ts);
1027 } elseif ($ts !== false) {
1028 $value= strftime($format, $ts);
1029 }
1030 } else {
1031 $value= strftime('%Y-%m-%d', $ts);
1032 }
1033 }
1034 break;
1035 case 'array' :
1036 if (is_string($value)) {
1037 $value= unserialize($value);
1038 }
1039 break;
1040 case 'json' :
1041 if (is_string($value) && strlen($value) > 1) {
1042 $value= $this->xpdo->fromJSON($value, true);
1043 }
1044 break;
1045 default :
1046 if (is_string($format) && !empty ($format)) {
1047 if (strpos($format, 're:') === 0) {
1048 if (!empty ($formatTemplate) && is_string($formatTemplate)) {
1049 $value= preg_replace(substr($format, 3), $formatTemplate, $value);
1050 }
1051 } else {
1052 $value= sprintf($format, $value);
1053 }
1054 }
1055 break;
1056 }
1057 }
1058 }
1059 }
1060 return $value;
1061 }
1062
1063 /**
1064 * Gets an object related to this instance by a foreign key relationship.
1065 *
1066 * Use this for 1:? (one:zero-or-one) or 1:1 relationships, which you can
1067 * distinguish by setting the nullability of the field representing the
1068 * foreign key.
1069 *
1070 * For all 1:* relationships for this instance, see {@link getMany()}.
1071 *
1072 * @see xPDOObject::getMany()
1073 * @see xPDOObject::addOne()
1074 * @see xPDOObject::addMany()
1075 *
1076 * @param string $alias Alias of the foreign class representing the related
1077 * object.
1078 * @param object $criteria xPDOCriteria object to get the related objects
1079 * @param boolean|integer $cacheFlag Indicates if the object should be
1080 * cached and optionally, by specifying an integer value, for how many
1081 * seconds.
1082 * @return xPDOObject|null The related object or null if no instance exists.
1083 */
1084 public function & getOne($alias, $criteria= null, $cacheFlag= true) {
1085 $object= null;
1086 if ($fkdef= $this->getFKDefinition($alias)) {
1087 $k= $fkdef['local'];
1088 $fk= $fkdef['foreign'];
1089 if (isset ($this->_relatedObjects[$alias])) {
1090 if (is_object($this->_relatedObjects[$alias])) {
1091 $object= & $this->_relatedObjects[$alias];
1092 return $object;
1093 }
1094 }
1095 if ($criteria === null) {
1096 $criteria= array ($fk => $this->get($k));
1097 if (isset($fkdef['criteria']) && isset($fkdef['criteria']['foreign'])) {
1098 $criteria= array($fkdef['criteria']['foreign'], $criteria);
1099 }
1100 }
1101 if ($object= $this->xpdo->getObject($fkdef['class'], $criteria, $cacheFlag)) {
1102 $this->_relatedObjects[$alias]= $object;
1103 }
1104 } else {
1105 $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Could not getOne: foreign key definition for alias {$alias} not found.");
1106 }
1107 return $object;
1108 }
1109
1110 /**
1111 * Gets a collection of objects related by aggregate or composite relations.
1112 *
1113 * @see xPDOObject::getOne()
1114 * @see xPDOObject::addOne()
1115 * @see xPDOObject::addMany()
1116 *
1117 * @param string $alias Alias of the foreign class representing the related
1118 * object.
1119 * @param object $criteria xPDOCriteria object to get the related objects
1120 * @param boolean|integer $cacheFlag Indicates if the objects should be
1121 * cached and optionally, by specifying an integer value, for how many
1122 * seconds.
1123 * @return array A collection of related objects or an empty array.
1124 */
1125 public function & getMany($alias, $criteria= null, $cacheFlag= true) {
1126 $collection= $this->_getRelatedObjectsByFK($alias, $criteria, $cacheFlag);
1127 return $collection;
1128 }
1129
1130 /**
1131 * Get an xPDOIterator for a collection of objects related by aggregate or composite relations.
1132 *
1133 * @param string $alias The alias of the relation.
1134 * @param null|array|xPDOCriteria $criteria A valid xPDO criteria expression.
1135 * @param bool|int $cacheFlag Indicates if the objects should be cached and optionally, by
1136 * specifying an integer values, for how many seconds.
1137 * @return bool|xPDOIterator An iterator for the collection or false if no relation is found.
1138 */
1139 public function getIterator($alias, $criteria= null, $cacheFlag= true) {
1140 $iterator = false;
1141 $fkMeta= $this->getFKDefinition($alias);
1142 if ($fkMeta) {
1143 $fkCriteria = isset($fkMeta['criteria']) && isset($fkMeta['criteria']['foreign']) ? $fkMeta['criteria']['foreign'] : null;
1144 if ($criteria === null) {
1145 $criteria= array($fkMeta['foreign'] => $this->get($fkMeta['local']));
1146 if ($fkCriteria !== null) {
1147 $criteria = array($fkCriteria, $criteria);
1148 }
1149 } else {
1150 $criteria= $this->xpdo->newQuery($fkMeta['class'], $criteria);
1151 $addCriteria = array("{$criteria->getAlias()}.{$fkMeta['foreign']}" => $this->get($fkMeta['local']));
1152 if ($fkCriteria !== null) {
1153 $fkAddCriteria = array();
1154 foreach ($fkCriteria as $fkCritKey => $fkCritVal) {
1155 if (is_numeric($fkCritKey)) continue;
1156 $fkAddCriteria["{$criteria->getAlias()}.{$fkCritKey}"] = $fkCritVal;
1157 }
1158 if (!empty($fkAddCriteria)) {
1159 $addCriteria = array($fkAddCriteria, $addCriteria);
1160 }
1161 }
1162 $criteria->andCondition($addCriteria);
1163 }
1164 $iterator = $this->xpdo->getIterator($fkMeta['class'], $criteria, $cacheFlag);
1165 }
1166 return $iterator;
1167 }
1168
1169 /**
1170 * Adds an object related to this instance by a foreign key relationship.
1171 *
1172 * @see xPDOObject::getOne()
1173 * @see xPDOObject::getMany()
1174 * @see xPDOObject::addMany()
1175 *
1176 * @param mixed &$obj A single object to be related to this instance.
1177 * @param string $alias The relation alias of the related object (only
1178 * required if more than one relation exists to the same foreign class).
1179 * @return boolean True if the related object was added to this object.
1180 */
1181 public function addOne(& $obj, $alias= '') {
1182 $added= false;
1183 if (is_object($obj)) {
1184 if (empty ($alias)) {
1185 if ($obj->_alias == $obj->_class) {
1186 $aliases = $this->_getAliases($obj->_class, 1);
1187 if (!empty($aliases)) {
1188 $obj->_alias = reset($aliases);
1189 }
1190 }
1191 $alias= $obj->_alias;
1192 }
1193 $fkMeta= $this->getFKDefinition($alias);
1194 if ($fkMeta && $fkMeta['cardinality'] === 'one') {
1195 $obj->_alias= $alias;
1196 $fk= $fkMeta['foreign'];
1197 $key= $fkMeta['local'];
1198 $owner= isset ($fkMeta['owner']) ? $fkMeta['owner'] : 'local';
1199 $kval= $this->get($key);
1200 $fkval= $obj->get($fk);
1201 if ($owner == 'local') {
1202 $fkCriteria = isset($fkMeta['criteria']) && isset($fkMeta['criteria']['foreign']) ? $fkMeta['criteria']['foreign'] : null;
1203 $obj->set($fk, $kval);
1204 if (is_array($fkCriteria)) {
1205 foreach ($fkCriteria as $fkCritKey => $fkCritVal) {
1206 if (is_numeric($fkCritKey)) continue;
1207 $obj->set($fkCritKey, $fkCritVal);
1208 }
1209 }
1210 }
1211 else {
1212 $this->set($key, $fkval);
1213 $fkCriteria = isset($fkMeta['criteria']) && isset($fkMeta['criteria']['local']) ? $fkMeta['criteria']['local'] : null;
1214 if (is_array($fkCriteria)) {
1215 foreach ($fkCriteria as $fkCritKey => $fkCritVal) {
1216 if (is_numeric($fkCritKey)) continue;
1217 $this->set($fkCritKey, $fkCritVal);
1218 }
1219 }
1220 }
1221
1222 $this->_relatedObjects[$obj->_alias]= $obj;
1223 $this->setDirty($key);
1224 $added= true;
1225 } else {
1226 $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Foreign key definition for class {$obj->class}, alias {$obj->_alias} not found, or cardinality is not 'one'.");
1227 }
1228 } else {
1229 $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Attempt to add a non-object to a relation with alias ({$alias})");
1230 }
1231 if (!$added) {
1232 $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Could not add related object! " . (is_object($obj) ? print_r($obj->toArray(), true) : ''));
1233 }
1234 return $added;
1235 }
1236
1237 /**
1238 * Adds an object or collection of objects related to this class.
1239 *
1240 * This method adds an object or collection of objects in a one-to-
1241 * many foreign key relationship with this object to the internal list of
1242 * related objects. By adding these related objects, you can cascade
1243 * {@link xPDOObject::save()}, {@link xPDOObject::remove()}, and other
1244 * operations based on the type of relationships defined.
1245 *
1246 * @see xPDOObject::addOne()
1247 * @see xPDOObject::getOne()
1248 * @see xPDOObject::getMany()
1249 *
1250 * @param mixed &$obj A single object or collection of objects to be related
1251 * to this instance via the intersection class.
1252 * @param string $alias An optional alias, required only for instances where
1253 * you have more than one relation defined to the same class.
1254 * @return boolean Indicates if the addMany was successful.
1255 */
1256 public function addMany(& $obj, $alias= '') {
1257 $added= false;
1258 if (!is_array($obj)) {
1259 if (is_object($obj)) {
1260 if (empty ($alias)) {
1261 if ($obj->_alias == $obj->_class) {
1262 $aliases = $this->_getAliases($obj->_class, 1);
1263 if (!empty($aliases)) {
1264 $obj->_alias = reset($aliases);
1265 }
1266 }
1267 $alias= $obj->_alias;
1268 }
1269 if ($fkMeta= $this->getFKDefinition($alias)) {
1270 $obj->_alias= $alias;
1271 if ($fkMeta['cardinality'] === 'many') {
1272 if ($obj->_new) {
1273 $objpk= '__new' . (isset ($this->_relatedObjects[$alias]) ? count($this->_relatedObjects[$alias]) : 0);
1274 } else {
1275 $objpk= $obj->getPrimaryKey();
1276 if (is_array($objpk)) {
1277 $objpk= implode('-', $objpk);
1278 }
1279 }
1280 $this->_relatedObjects[$alias][$objpk]= $obj;
1281 if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, 'Added related object with alias: ' . $alias . ' and pk: ' . $objpk . "\n" . print_r($obj->toArray('', true), true));
1282 $added= true;
1283 }
1284 }
1285 }
1286 } else {
1287 foreach ($obj as $o) {
1288 $added= $this->addMany($o, $alias);
1289 }
1290 }
1291 return $added;
1292 }
1293
1294 /**
1295 * Persist new or changed objects to the database container.
1296 *
1297 * Inserts or updates the database record representing this object and any
1298 * new or changed related object records. Both aggregate and composite
1299 * related objects will be saved as appropriate, before or following the
1300 * save operation on the controlling instance.
1301 *
1302 * @param boolean|integer $cacheFlag Indicates if the saved object(s) should
1303 * be cached and optionally, by specifying an integer value, for how many
1304 * seconds before expiring. Overrides the cacheFlag for the object(s).
1305 * @return boolean Returns true on success, false on failure.
1306 */
1307 public function save($cacheFlag= null) {
1308 if ($this->isLazy()) {
1309 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Attempt to save lazy object: ' . print_r($this->toArray('', true), 1));
1310 return false;
1311 }
1312 $result= true;
1313 $sql= '';
1314 $pk= $this->getPrimaryKey();
1315 $pkn= $this->getPK();
1316 $pkGenerated= false;
1317 if ($this->isNew()) {
1318 $this->setDirty();
1319 }
1320 if ($this->getOption(xPDO::OPT_VALIDATE_ON_SAVE)) {
1321 if (!$this->validate()) {
1322 return false;
1323 }
1324 }
1325 if (!$this->xpdo->getConnection(array(xPDO::OPT_CONN_MUTABLE => true))) {
1326 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not get connection for writing data", '', __METHOD__, __FILE__, __LINE__);
1327 return false;
1328 }
1329 $this->_saveRelatedObjects();
1330 if (!empty ($this->_dirty)) {
1331 $cols= array ();
1332 $bindings= array ();
1333 $updateSql= array ();
1334 foreach (array_keys($this->_dirty) as $_k) {
1335 if (!array_key_exists($_k, $this->_fieldMeta)) {
1336 continue;
1337 }
1338 if (isset($this->_fieldMeta[$_k]['generated'])) {
1339 if (!$this->_new || !isset($this->_fields[$_k]) || empty($this->_fields[$_k])) {
1340 $pkGenerated= true;
1341 continue;
1342 }
1343 }
1344 if ($this->_fieldMeta[$_k]['phptype'] === 'password') {
1345 $this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password');
1346 }
1347 $fieldType= PDO::PARAM_STR;
1348 $fieldValue= $this->_fields[$_k];
1349 if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') {
1350 $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
1351 continue;
1352 }
1353 elseif ($fieldValue === null || $fieldValue === 'NULL') {
1354 if ($this->_new) continue;
1355 $fieldType= PDO::PARAM_NULL;
1356 $fieldValue= null;
1357 }
1358 elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('timestamp', 'datetime')) && in_array($fieldValue, $this->xpdo->driver->_currentTimestamps, true)) {
1359 $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S');
1360 continue;
1361 }
1362 elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('date')) && in_array($fieldValue, $this->xpdo->driver->_currentDates, true)) {
1363 $this->_fields[$_k]= strftime('%Y-%m-%d');
1364 continue;
1365 }
1366 elseif ($this->_fieldMeta[$_k]['phptype'] == 'timestamp' && preg_match('/int/i', $this->_fieldMeta[$_k]['dbtype'])) {
1367 $fieldType= PDO::PARAM_INT;
1368 }
1369 elseif (!in_array($this->_fieldMeta[$_k]['phptype'], array ('string','password','datetime','timestamp','date','time','array','json'))) {
1370 $fieldType= PDO::PARAM_INT;
1371 }
1372 if ($this->_new) {
1373 $cols[$_k]= $this->xpdo->escape($_k);
1374 $bindings[":{$_k}"]['value']= $fieldValue;
1375 $bindings[":{$_k}"]['type']= $fieldType;
1376 } else {
1377 $bindings[":{$_k}"]['value']= $fieldValue;
1378 $bindings[":{$_k}"]['type']= $fieldType;
1379 $updateSql[]= $this->xpdo->escape($_k) . " = :{$_k}";
1380 }
1381 }
1382 if ($this->_new) {
1383 $sql= "INSERT INTO {$this->_table} (" . implode(', ', array_values($cols)) . ") VALUES (" . implode(', ', array_keys($bindings)) . ")";
1384 } else {
1385 if ($pk && $pkn) {
1386 if (is_array($pkn)) {
1387 $iteration= 0;
1388 $where= '';
1389 foreach ($pkn as $k => $v) {
1390 $vt= PDO::PARAM_INT;
1391 if ($this->_fieldMeta[$k]['phptype'] == 'string') {
1392 $vt= PDO::PARAM_STR;
1393 }
1394 if ($iteration) {
1395 $where .= " AND ";
1396 }
1397 $where .= $this->xpdo->escape($k) . " = :{$k}";
1398 $bindings[":{$k}"]['value']= $this->_fields[$k];
1399 $bindings[":{$k}"]['type']= $vt;
1400 $iteration++;
1401 }
1402 } else {
1403 $pkn= $this->getPK();
1404 $pkt= PDO::PARAM_INT;
1405 if ($this->_fieldMeta[$pkn]['phptype'] == 'string') {
1406 $pkt= PDO::PARAM_STR;
1407 }
1408 $bindings[":{$pkn}"]['value']= $pk;
1409 $bindings[":{$pkn}"]['type']= $pkt;
1410 $where= $this->xpdo->escape($pkn) . ' = :' . $pkn;
1411 }
1412 if (!empty ($updateSql)) {
1413 $sql= "UPDATE {$this->_table} SET " . implode(',', $updateSql) . " WHERE {$where}";
1414 }
1415 }
1416 }
1417 if (!empty ($sql) && $criteria= new xPDOCriteria($this->xpdo, $sql)) {
1418 if ($criteria->prepare()) {
1419 if (!empty ($bindings)) {
1420 $criteria->bind($bindings, true, false);
1421 }
1422 if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Executing SQL:\n{$sql}\nwith bindings:\n" . print_r($bindings, true));
1423 if (!$result= $criteria->stmt->execute()) {
1424 $errorInfo= $criteria->stmt->errorInfo();
1425 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n" . $criteria->toSQL() . "\n" . print_r($errorInfo, true));
1426 if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $this->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) {
1427 if ($this->xpdo->getManager() && $this->xpdo->manager->createObjectContainer($this->_class) === true) {
1428 if (!$result= $criteria->stmt->execute()) {
1429 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n{$sql}\n");
1430 }
1431 } else {
1432 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $this->xpdo->errorCode() . " attempting to create object container for class {$this->_class}:\n" . print_r($this->xpdo->errorInfo(), true));
1433 }
1434 }
1435 }
1436 } else {
1437 $result= false;
1438 }
1439 if ($result) {
1440 if ($pkn && !$pk) {
1441 if ($pkGenerated) {
1442 $this->_fields[$this->getPK()]= $this->xpdo->lastInsertId();
1443 }
1444 $pk= $this->getPrimaryKey();
1445 }
1446 if ($pk || !$this->getPK()) {
1447 $this->_dirty= array();
1448 $this->_validated= array();
1449 $this->_new= false;
1450 }
1451 $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_SAVE);
1452 if ($callback && is_callable($callback)) {
1453 call_user_func($callback, array('className' => $this->_class, 'criteria' => $criteria, 'object' => $this));
1454 }
1455 if ($this->xpdo->_cacheEnabled && $pk && ($cacheFlag || ($cacheFlag === null && $this->_cacheFlag))) {
1456 $cacheKey= $this->xpdo->newQuery($this->_class, $pk, $cacheFlag);
1457 if (is_bool($cacheFlag)) {
1458 $expires= 0;
1459 } else {
1460 $expires= intval($cacheFlag);
1461 }
1462 $this->xpdo->toCache($cacheKey, $this, $expires, array('modified' => true));
1463 }
1464 }
1465 }
1466 }
1467 $this->_saveRelatedObjects();
1468 if ($result) {
1469 $this->_dirty= array ();
1470 $this->_validated= array ();
1471 }
1472 return $result;
1473 }
1474
1475 /**
1476 * Searches for any related objects with pending changes to save.
1477 *
1478 * @access protected
1479 * @uses xPDOObject::_saveRelatedObject()
1480 * @return integer The number of related objects processed.
1481 */
1482 protected function _saveRelatedObjects() {
1483 $saved= 0;
1484 if (!empty ($this->_relatedObjects)) {
1485 foreach ($this->_relatedObjects as $alias => $ro) {
1486 $objects= array ();
1487 if (is_object($ro)) {
1488 $primaryKey= $ro->_new ? '__new' : $ro->getPrimaryKey();
1489 if (is_array($primaryKey)) $primaryKey= implode('-', $primaryKey);
1490 $objects[$primaryKey]= & $ro;
1491 $cardinality= 'one';
1492 }
1493 elseif (is_array($ro)) {
1494 $objects= $ro;
1495 $cardinality= 'many';
1496 }
1497 if (!empty($objects)) {
1498 foreach ($objects as $pk => $obj) {
1499 if ($fkMeta= $this->getFKDefinition($alias)) {
1500 if ($this->_saveRelatedObject($obj, $fkMeta)) {
1501 if ($cardinality == 'many') {
1502 $newPk= $obj->getPrimaryKey();
1503 if (is_array($newPk)) $newPk= implode('-', $newPk);
1504 if ($pk != $newPk) {
1505 $this->_relatedObjects[$alias][$newPk]= $obj;
1506 unset($this->_relatedObjects[$alias][$pk]);
1507 }
1508 }
1509 $saved++;
1510 }
1511 }
1512 }
1513 }
1514 }
1515 }
1516 return $saved;
1517 }
1518
1519 /**
1520 * Save a related object with pending changes.
1521 *
1522 * This function is also responsible for setting foreign keys when new
1523 * related objects are being saved, as well as local keys when the host
1524 * object is new and needs the foreign key.
1525 *
1526 * @access protected
1527 * @param xPDOObject &$obj A reference to the related object.
1528 * @param array $fkMeta The meta data representing the relation.
1529 * @return boolean True if a related object was dirty and saved successfully.
1530 */
1531 protected function _saveRelatedObject(& $obj, $fkMeta) {
1532 $saved= false;
1533 $local= $fkMeta['local'];
1534 $foreign= $fkMeta['foreign'];
1535 $cardinality= $fkMeta['cardinality'];
1536 $owner= isset ($fkMeta['owner']) ? $fkMeta['owner'] : '';
1537 if (!$owner) {
1538 $owner= $cardinality === 'many' ? 'foreign' : 'local';
1539 }
1540 $criteria = isset($fkMeta['criteria']) ? $fkMeta['criteria'] : null;
1541 if ($owner === 'local' && $fk= $this->get($local)) {
1542 $obj->set($foreign, $fk);
1543 if (isset($criteria['foreign']) && is_array($criteria['foreign'])) {
1544 foreach ($criteria['foreign'] as $critKey => $critVal) {
1545 if (is_numeric($critKey)) continue;
1546 $obj->set($critKey, $critVal);
1547 }
1548 }
1549 $saved= $obj->save();
1550 } elseif ($owner === 'foreign') {
1551 if ($obj->isNew() || !empty($obj->_dirty)) {
1552 $saved= $obj->save();
1553 }
1554 $fk= $obj->get($foreign);
1555 if ($fk) {
1556 $this->set($local, $fk);
1557 if (isset($criteria['local']) && is_array($criteria['local'])) {
1558 foreach ($criteria['local'] as $critKey => $critVal) {
1559 if (is_numeric($critKey)) continue;
1560 $this->set($critKey, $critVal);
1561 }
1562 }
1563 }
1564 }
1565 if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG , ($saved ? 'Successfully saved' : 'Could not save') . " related object\nMain object: " . print_r($this->toArray('', true), true) . "\nRelated Object: " . print_r($obj->toArray('', true), true));
1566 return $saved;
1567 }
1568
1569 /**
1570 * Remove the persistent instance of an object permanently.
1571 *
1572 * Deletes the persistent object isntance stored in the database when
1573 * called, including any dependent objects defined by composite foreign key
1574 * relationships.
1575 *
1576 * @todo Implement some way to reassign ownership of related composite
1577 * objects when remove is called, perhaps by passing another object
1578 * instance as an optional parameter, or creating a separate method.
1579 *
1580 * @param array $ancestors Keeps track of classes which have already been
1581 * removed to prevent loop with circular references.
1582 * @return boolean Returns true on success, false on failure.
1583 */
1584 public function remove(array $ancestors= array ()) {
1585 $result= false;
1586 $pk= $this->getPrimaryKey();
1587 if ($pk && $this->xpdo->getConnection(array(xPDO::OPT_CONN_MUTABLE => true))) {
1588 if (!empty ($this->_composites)) {
1589 $current= array ($this->_class, $this->_alias);
1590 foreach ($this->_composites as $compositeAlias => $composite) {
1591 if (in_array($compositeAlias, $ancestors) || in_array($composite['class'], $ancestors)) {
1592 continue;
1593 }
1594 if ($composite['cardinality'] === 'many') {
1595 if ($many= $this->getMany($compositeAlias)) {
1596 foreach ($many as $one) {
1597 $ancestors[]= $compositeAlias;
1598 $newAncestors= $ancestors + $current;
1599 if (!$one->remove($newAncestors)) {
1600 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error removing dependent object: " . print_r($one->toArray('', true), true));
1601 }
1602 }
1603 unset($many);
1604 }
1605 }
1606 elseif ($one= $this->getOne($compositeAlias)) {
1607 $ancestors[]= $compositeAlias;
1608 $newAncestors= $ancestors + $current;
1609 if (!$one->remove($newAncestors)) {
1610 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error removing dependent object: " . print_r($one->toArray('', true), true));
1611 }
1612 unset($one);
1613 }
1614 }
1615 }
1616 $delete= $this->xpdo->newQuery($this->_class);
1617 $delete->command('DELETE');
1618 $delete->where($pk);
1619 // $delete->limit(1);
1620 $stmt= $delete->prepare();
1621 if (is_object($stmt)) {
1622 if (!$result= $stmt->execute()) {
1623 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not delete from ' . $this->_table . '; primary key specified was ' . print_r($pk, true) . "\n" . print_r($stmt->errorInfo(), true));
1624 } else {
1625 $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_REMOVE);
1626 if ($callback && is_callable($callback)) {
1627 call_user_func($callback, array('className' => $this->_class, 'criteria' => $delete, 'object' => $this));
1628 }
1629 if ($this->xpdo->_cacheEnabled) {
1630 $cacheKey= is_array($pk) ? implode('_', $pk) : $pk;
1631 $this->xpdo->toCache($this->xpdo->getTableClass($this->_class) . '_' . $cacheKey, null, 0, array('modified' => true));
1632 }
1633 $this->xpdo->log(xPDO::LOG_LEVEL_INFO, "Removed {$this->_class} instance with primary key " . print_r($pk, true));
1634 }
1635 } else {
1636 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not build criteria to delete from ' . $this->_table . '; primary key specified was ' . print_r($pk, true));
1637 }
1638 }
1639 return $result;
1640 }
1641
1642 /**
1643 * Gets the value (or values) of the primary key field(s) for the object.
1644 *
1645 * @param boolean $validateCompound If any of the keys in a compound primary key are empty
1646 * or null, and the default value is not allowed to be null, do not return an array, instead
1647 * return null; the default is true
1648 * @return mixed The string (or an array) representing the value(s) of the
1649 * primary key field(s) for this instance.
1650 */
1651 public function getPrimaryKey($validateCompound= true) {
1652 $value= null;
1653 if ($pk= $this->getPK()) {
1654 if (is_array($pk)) {
1655 foreach ($pk as $k) {
1656 $_pk= $this->get($k);
1657 if (($_pk && strtolower($_pk) !== 'null') || !$validateCompound) {
1658 $value[]= $_pk;
1659 } else {
1660 if (isset ($this->_fieldMeta[$k]['default'])) {
1661 $value[]= $this->_fieldMeta[$k]['default'];
1662 $this->_fields[$k]= $this->_fieldMeta[$k]['default'];
1663 }
1664 elseif (isset ($this->_fieldMeta[$k]['null']) && $this->_fieldMeta[$k]['null']) {
1665 $value[]= null;
1666 }
1667 else {
1668 $value= null;
1669 break;
1670 }
1671 }
1672 }
1673 } else {
1674 $value= $this->get($pk);
1675 }
1676 }
1677 if (!$value && $this->xpdo->getDebug() === true) {
1678 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "No primary key value found for pk definition: " . print_r($this->getPK(), true));
1679 }
1680 return $value;
1681 }
1682
1683 /**
1684 * Gets the name (or names) of the primary key field(s) for the object.
1685 *
1686 * @return mixed The string (or an array of strings) representing the name(s)
1687 * of the primary key field(s) for this instance.
1688 */
1689 public function getPK() {
1690 if ($this->_pk === null) {
1691 $this->_pk= $this->xpdo->getPK($this->_class);
1692 }
1693 return $this->_pk;
1694 }
1695
1696 /**
1697 * Gets the type of the primary key field for the object.
1698 *
1699 * @return string The type of the primary key field for this instance.
1700 */
1701 public function getPKType() {
1702 if ($this->_pktype === null) {
1703 if ($this->_pk === null) {
1704 $this->getPK();
1705 }
1706 $this->_pktype= $this->xpdo->getPKType($this->_class);
1707 }
1708 return $this->_pktype;
1709 }
1710
1711 /**
1712 * Get the name of a class related by foreign key to a specified field key.
1713 *
1714 * This is generally used to lookup classes involved in one-to-one
1715 * relationships with the current object.
1716 *
1717 * @param string $k The field name or key to lookup a related class for.
1718 */
1719 public function getFKClass($k) {
1720 $fkclass= null;
1721 $k = $this->getField($k, true);
1722 if (is_string($k)) {
1723 if (!empty ($this->_aggregates)) {
1724 foreach ($this->_aggregates as $aggregateAlias => $aggregate) {
1725 if ($aggregate['local'] === $k) {
1726 $fkclass= $aggregate['class'];
1727 break;
1728 }
1729 }
1730 }
1731 if (!$fkclass && !empty ($this->_composites)) {
1732 foreach ($this->_composites as $compositeAlias => $composite) {
1733 if ($composite['local'] === $k) {
1734 $fkclass= $composite['class'];
1735 break;
1736 }
1737 }
1738 }
1739 $fkclass= $this->xpdo->loadClass($fkclass);
1740 }
1741 if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Returning foreign key class {$fkclass} for column {$k}");
1742 return $fkclass;
1743 }
1744
1745 /**
1746 * Get a foreign key definition for a specific classname.
1747 *
1748 * This is generally used to lookup classes in a one-to-many relationship
1749 * with the current object.
1750 *
1751 * @param string $alias Alias of the related class to lookup a foreign key
1752 * definition from.
1753 * @return array A foreign key definition.
1754 */
1755 public function getFKDefinition($alias) {
1756 return $this->xpdo->getFKDefinition($this->_class, $alias);
1757 }
1758
1759 /**
1760 * Gets a field name as represented in the database container.
1761 *
1762 * This gets the name of the field, fully-qualified by either the object
1763 * table name or a specified alias, and properly quoted.
1764 *
1765 * @param string $k The simple name of the field.
1766 * @param string $alias An optional alias for the table in a specific query.
1767 * @return string The name of the field, qualified with the table name or an
1768 * optional table alias.
1769 */
1770 public function getFieldName($k, $alias= null) {
1771 if ($this->fieldNames === null) {
1772 $this->_initFields();
1773 }
1774 $name= null;
1775 $k = $this->getField($k, true);
1776 if (is_string($k) && isset ($this->fieldNames[$k])) {
1777 $name= $this->fieldNames[$k];
1778 }
1779 if ($name !== null && $alias !== null) {
1780 $name= str_replace("{$this->_table}.", "{$alias}.", $name);
1781 }
1782 return $name;
1783 }
1784
1785 /**
1786 * Get a field name, looking up any by alias if not an actual field.
1787 *
1788 * @param string $key The field name or alias to translate to the actual field name.
1789 * @param bool $validate If true, the method will return false if the field or an alias
1790 * of it is not found. Otherwise, the key is returned as passed.
1791 * @return string|bool The actual field name, the key as passed, or false if not a field
1792 * or alias and validate is true.
1793 */
1794 public function getField($key, $validate = false) {
1795 $field = $key;
1796 if (!array_key_exists($key, $this->_fieldMeta)) {
1797 if (array_key_exists($key, $this->_fieldAliases)) {
1798 $field = $this->_fieldAliases[$key];
1799 } elseif ($validate === true) {
1800 $field = false;
1801 }
1802 }
1803 return $field;
1804 }
1805
1806 /**
1807 * Load a graph of related objects to the current object.
1808 *
1809 * @param boolean|string|array|integer $graph An option to tell how to deal with related objects. If integer, will
1810 * traverse related objects up to a $graph level of depth and load them to the object.
1811 * If an array, will traverse required related object and load them to the object.
1812 * If true, will traverse the entire graph and append all related objects to the object (default behavior).
1813 * @param xPDOCriteria|array|string|integer $criteria A valid xPDO criteria representation.
1814 * @param boolean|integer $cacheFlag Indicates if the objects should be cached and optionally, by specifying an
1815 * integer value, for how many seconds.
1816 * @return array|boolean The graph that was loaded or false if nothing was loaded.
1817 */
1818 public function getGraph($graph = true, $criteria = null, $cacheFlag = true) {
1819 /* graph is true, get all relations to max depth */
1820 if ($graph === true) {
1821 $graph = $this->xpdo->getGraph($this->_class);
1822 }
1823 /* graph is an int, get relations to depth of graph */
1824 if (is_int($graph)) {
1825 $graph = $this->xpdo->getGraph($this->_class, $graph);
1826 }
1827 /* graph defined as JSON, convert to array */
1828 if (is_string($graph)) {
1829 $graph= $this->xpdo->fromJSON($graph);
1830 }
1831 /* graph as an array */
1832 if (is_array($graph)) {
1833 foreach ($graph as $alias => $branch) {
1834 $fkMeta = $this->getFKDefinition($alias);
1835 if ($fkMeta) {
1836 $fkCriteria = isset($fkMeta['criteria']) && isset($fkMeta['criteria']['foreign']) ? $fkMeta['criteria']['foreign'] : null;
1837 if ($criteria === null) {
1838 $query= array($fkMeta['foreign'] => $this->get($fkMeta['local']));
1839 if ($fkCriteria !== null) {
1840 $query= array($fkCriteria, $query);
1841 }
1842 } else {
1843 $query= $this->xpdo->newQuery($fkMeta['class'], $criteria);
1844 $addCriteria= array("{$query->getAlias()}.{$fkMeta['foreign']}" => $this->get($fkMeta['local']));
1845 if ($fkCriteria !== null) {
1846 $fkAddCriteria = array();
1847 foreach ($fkCriteria as $fkCritKey => $fkCritVal) {
1848 if (is_numeric($fkCritKey)) continue;
1849 $fkAddCriteria["{$criteria->getAlias()}.{$fkCritKey}"] = $fkCritVal;
1850 }
1851 if (!empty($fkAddCriteria)) {
1852 $addCriteria = array($fkAddCriteria, $addCriteria);
1853 }
1854 }
1855 $query->andCondition($addCriteria);
1856 }
1857 $collection = $this->xpdo->call($fkMeta['class'], 'loadCollectionGraph', array(
1858 &$this->xpdo,
1859 $fkMeta['class'],
1860 $branch,
1861 $query,
1862 $cacheFlag
1863 ));
1864 if (!empty($collection)) {
1865 if ($fkMeta['cardinality'] == 'many') {
1866 $this->_relatedObjects[$alias] = $collection;
1867 } else {
1868 $this->_relatedObjects[$alias] = reset($collection);
1869 }
1870 }
1871 }
1872 }
1873 } else {
1874 $graph = false;
1875 }
1876 return $graph;
1877 }
1878
1879 /**
1880 * Copies the object fields and corresponding values to an associative array.
1881 *
1882 * @param string $keyPrefix An optional prefix to prepend to the field values.
1883 * @param boolean $rawValues An optional flag indicating if you want the raw values instead of
1884 * those returned by the {@link xPDOObject::get()} function.
1885 * @param boolean $excludeLazy An option flag indicating if you want to exclude lazy fields from
1886 * the resulting array; the default behavior is to include them which means the object will
1887 * query the database for the lazy fields before providing the value.
1888 * @param boolean|integer|string|array $includeRelated Describes if and how to include loaded related object fields.
1889 * As an integer all loaded related objects in the graph up to that level of depth will be included.
1890 * As a string, only loaded related objects matching the JSON graph representation will be included.
1891 * As an array, only loaded related objects matching the graph array will be included.
1892 * As boolean true, all currently loaded related objects will be included.
1893 * @return array An array representation of the object fields/values.
1894 */
1895 public function toArray($keyPrefix= '', $rawValues= false, $excludeLazy= false, $includeRelated= false) {
1896 $objArray= null;
1897 if (is_array($this->_fields)) {
1898 $keys= array_keys($this->_fields);
1899 if (!$excludeLazy && $this->isLazy()) {
1900 $this->_loadFieldData($this->_lazy);
1901 }
1902 foreach ($keys as $key) {
1903 if (!$excludeLazy || !$this->isLazy($key)) {
1904 $objArray[$keyPrefix . $key]= $rawValues ? $this->_fields[$key] : $this->get($key);
1905 }
1906 }
1907 }
1908 if (!empty($includeRelated)) {
1909 $graph = null;
1910 if (is_int($includeRelated) && $includeRelated > 0) {
1911 $graph = $this->xpdo->getGraph($this->_class, $includeRelated);
1912 } elseif (is_string($includeRelated)) {
1913 $graph = $this->xpdo->fromJSON($includeRelated);
1914 } elseif (is_array($includeRelated)) {
1915 $graph = $includeRelated;
1916 }
1917 if ($includeRelated === true || is_array($graph)) {
1918 foreach ($this->_relatedObjects as $alias => $branch) {
1919 if ($includeRelated === true || array_key_exists($alias, $graph)) {
1920 if (is_array($branch)){
1921 foreach($branch as $pk => $obj){
1922 $objArray[$alias][$pk] = $obj->toArray($keyPrefix, $rawValues, $excludeLazy, $includeRelated === true ? true : $graph[$alias]);
1923 }
1924 } elseif ($branch instanceof xPDOObject) {
1925 $objArray[$alias] = $branch->toArray($keyPrefix, $rawValues, $excludeLazy, $includeRelated === true ? true : $graph[$alias]);
1926 }
1927 }
1928 }
1929 }
1930 }
1931 return $objArray;
1932 }
1933
1934 /**
1935 * Sets object fields from an associative array of key => value pairs.
1936 *
1937 * @param array $fldarray An associative array of key => values.
1938 * @param string $keyPrefix Specify an optional prefix to strip from all array
1939 * keys in fldarray.
1940 * @param boolean $setPrimaryKeys Optional param to set generated primary keys.
1941 * @param boolean $rawValues Optional way to set values without calling the
1942 * {@link xPDOObject::set()} method.
1943 * @param boolean $adhocValues Optional way to set adhoc values so that all the values of
1944 * fldarray become object vars.
1945 */
1946 public function fromArray($fldarray, $keyPrefix= '', $setPrimaryKeys= false, $rawValues= false, $adhocValues= false) {
1947 if (is_array($fldarray)) {
1948 $pkSet= false;
1949 $generatedKey= false;
1950 reset($fldarray);
1951 while (list ($key, $val)= each($fldarray)) {
1952 if (!empty ($keyPrefix)) {
1953 $prefixPos= strpos($key, $keyPrefix);
1954 if ($prefixPos === 0) {
1955 $key= substr($key, strlen($keyPrefix));
1956 } else {
1957 continue;
1958 }
1959 if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Stripped prefix {$keyPrefix} to produce key {$key}");
1960 }
1961 $key = $this->getField($key);
1962 if (isset ($this->_fieldMeta[$key]['index']) && $this->_fieldMeta[$key]['index'] == 'pk') {
1963 if ($setPrimaryKeys) {
1964 if (isset ($this->_fieldMeta[$key]['generated'])) {
1965 $generatedKey= true;
1966 }
1967 if ($this->_new) {
1968 if ($rawValues || $generatedKey) {
1969 $this->_setRaw($key, $val);
1970 } else {
1971 $this->set($key, $val);
1972 }
1973 $pkSet= true;
1974 }
1975 }
1976 }
1977 elseif (isset ($this->_fieldMeta[$key])) {
1978 if ($rawValues) {
1979 $this->_setRaw($key, $val);
1980 } else {
1981 $this->set($key, $val);
1982 }
1983 }
1984 elseif ($adhocValues || $this->getOption(xPDO::OPT_HYDRATE_ADHOC_FIELDS)) {
1985 if ($rawValues) {
1986 $this->_setRaw($key, $val);
1987 } else {
1988 $this->set($key, $val);
1989 }
1990 }
1991 if ($this->isLazy($key)) {
1992 $this->_lazy = array_diff($this->_lazy, array($key));
1993 }
1994 }
1995 }
1996 }
1997
1998 /**
1999 * Add a validation rule to an object field for this instance.
2000 *
2001 * @param string $field The field key to apply the rule to.
2002 * @param string $name A name to identify the rule.
2003 * @param string $type The type of rule.
2004 * @param string $rule The rule definition.
2005 * @param array $parameters Any input parameters for the rule.
2006 */
2007 public function addValidationRule($field, $name, $type, $rule, array $parameters= array()) {
2008 $field = $this->getField($field);
2009 if (is_string($field)) {
2010 if (!$this->_validationLoaded) $this->_loadValidation();
2011 if (!isset($this->_validationRules[$field])) $this->_validationRules[$field]= array();
2012 $this->_validationRules[$field][$name]= array(
2013 'type' => $type,
2014 'rule' => $rule,
2015 'parameters' => array()
2016 );
2017 foreach ($parameters as $paramKey => $paramValue) {
2018 $this->_validationRules[$field][$name]['parameters'][$paramKey]= $paramValue;
2019 }
2020 }
2021 }
2022
2023 /**
2024 * Remove one or more validation rules from this instance.
2025 *
2026 * @param string $field An optional field name to remove rules from. If not
2027 * specified or null, all rules from all columns will be removed.
2028 * @param array $rules An optional array of rule names to remove if a single
2029 * field is specified. If $field is null, this parameter is ignored.
2030 */
2031 public function removeValidationRules($field = null, array $rules = array()) {
2032 if (!$this->_validationLoaded) $this->_loadValidation();
2033 if (empty($rules) && is_string($field)) {
2034 unset($this->_validationRules[$this->getField($field)]);
2035 } elseif (empty($rules) && is_null($field)) {
2036 $this->_validationRules = array();
2037 } elseif (is_array($rules) && !empty($rules) && is_string($field)) {
2038 $field = $this->getField($field);
2039 foreach ($rules as $name) {
2040 unset($this->_validationRules[$field][$name]);
2041 }
2042 }
2043 }
2044
2045 /**
2046 * Get the xPDOValidator class configured for this instance.
2047 *
2048 * @return string|boolean The xPDOValidator instance or false if it could
2049 * not be loaded.
2050 */
2051 public function getValidator() {
2052 if (!is_object($this->_validator)) {
2053 $validatorClass = $this->xpdo->loadClass('validation.xPDOValidator', XPDO_CORE_PATH, true, true);
2054 if ($derivedClass = $this->getOption(xPDO::OPT_VALIDATOR_CLASS, null, '')) {
2055 if ($derivedClass = $this->xpdo->loadClass($derivedClass, '', false, true)) {
2056 $validatorClass = $derivedClass;
2057 }
2058 }
2059 if ($validatorClass) {
2060 $this->_validator= new $validatorClass($this);
2061 }
2062 }
2063 return $this->_validator;
2064 }
2065
2066 /**
2067 * Used to load validation from the object map.
2068 *
2069 * @access public
2070 * @param boolean $reload Indicates if the schema validation rules should be
2071 * reloaded.
2072 */
2073 public function _loadValidation($reload= false) {
2074 if (!$this->_validationLoaded || $reload == true) {
2075 $validation= $this->xpdo->getValidationRules($this->_class);
2076 $this->_validationLoaded= true;
2077 foreach ($validation as $column => $rules) {
2078 foreach ($rules as $name => $rule) {
2079 $parameters = array_diff($rule, array($rule['type'], $rule['rule']));
2080 $this->addValidationRule($column, $name, $rule['type'], $rule['rule'], $parameters);
2081 }
2082 }
2083 }
2084 }
2085
2086 /**
2087 * Validate the field values using an xPDOValidator.
2088 *
2089 * @param array $options An array of options to pass to the validator.
2090 * @return boolean True if validation was successful.
2091 */
2092 public function validate(array $options = array()) {
2093 $validated= false;
2094 if ($validator= $this->getValidator()) {
2095 $validated= $this->_validator->validate($options);
2096 if ($this->xpdo->getDebug() === true) {
2097 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Validator class executed, result = " . print_r($validated, true));
2098 }
2099 } else {
2100 if ($this->xpdo->getDebug() === true) {
2101 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "No validator found for {$this->_class} instance.");
2102 }
2103 $validated= true;
2104 }
2105 return $validated;
2106 }
2107
2108 /**
2109 * Indicates if the object or specified field has been validated.
2110 *
2111 * @param string $key Optional key to check for specific validation.
2112 * @return boolean True if the object or specified field has been fully
2113 * validated successfully.
2114 */
2115 public function isValidated($key= '') {
2116 $unvalidated = array_diff($this->_dirty, $this->_validated);
2117 if (empty($key)) {
2118 $validated = (count($unvalidated) > 0);
2119 } else {
2120 $validated = !in_array($this->getField($key), $unvalidated);
2121 }
2122 return $validated;
2123 }
2124
2125 /**
2126 * Indicates if the object or specified field is lazy.
2127 *
2128 * @param string $key Optional key to check for laziness.
2129 * @return boolean True if the field specified or if any field is lazy if no
2130 * field is specified.
2131 */
2132 public function isLazy($key= '') {
2133 $lazy = false;
2134 if (empty($key)) {
2135 $lazy = (count($this->_lazy) > 0);
2136 } else {
2137 $key = $this->getField($key, true);
2138 if ($key !== false) {
2139 $lazy = in_array($key, $this->_lazy);
2140 }
2141 }
2142 return $lazy;
2143 }
2144
2145 /**
2146 * Gets related objects by a foreign key and specified criteria.
2147 *
2148 * @access protected
2149 * @param string $alias The alias representing the relationship.
2150 * @param mixed An optional xPDO criteria expression.
2151 * @param boolean|integer Indicates if the saved object(s) should
2152 * be cached and optionally, by specifying an integer value, for how many
2153 * seconds before expiring. Overrides the cacheFlag for the object.
2154 * @return array A collection of objects matching the criteria.
2155 */
2156 protected function & _getRelatedObjectsByFK($alias, $criteria= null, $cacheFlag= true) {
2157 $collection= array ();
2158 if (isset($this->_relatedObjects[$alias]) && (is_object($this->_relatedObjects[$alias]) || (is_array($this->_relatedObjects[$alias]) && !empty ($this->_relatedObjects[$alias])))) {
2159 $collection= & $this->_relatedObjects[$alias];
2160 } else {
2161 $fkMeta= $this->getFKDefinition($alias);
2162 if ($fkMeta) {
2163 $fkCriteria = isset($fkMeta['criteria']) && isset($fkMeta['criteria']['foreign']) ? $fkMeta['criteria']['foreign'] : null;
2164 if ($criteria === null) {
2165 $criteria= array($fkMeta['foreign'] => $this->get($fkMeta['local']));
2166 if ($fkCriteria !== null) {
2167 $criteria= array($fkCriteria, $criteria);
2168 }
2169 } else {
2170 $criteria= $this->xpdo->newQuery($fkMeta['class'], $criteria);
2171 $addCriteria = array("{$criteria->getAlias()}.{$fkMeta['foreign']}" => $this->get($fkMeta['local']));
2172 if ($fkCriteria !== null) {
2173 $fkAddCriteria = array();
2174 foreach ($fkCriteria as $fkCritKey => $fkCritVal) {
2175 if (is_numeric($fkCritKey)) continue;
2176 $fkAddCriteria["{$criteria->getAlias()}.{$fkCritKey}"] = $fkCritVal;
2177 }
2178 if (!empty($fkAddCriteria)) {
2179 $addCriteria = array($fkAddCriteria, $addCriteria);
2180 }
2181 }
2182 $criteria->andCondition($addCriteria);
2183 }
2184 if ($collection= $this->xpdo->getCollection($fkMeta['class'], $criteria, $cacheFlag)) {
2185 $this->_relatedObjects[$alias]= array_merge($this->_relatedObjects[$alias], $collection);
2186 }
2187 }
2188 }
2189 if ($this->xpdo->getDebug() === true) {
2190 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "_getRelatedObjectsByFK :: {$alias} :: " . (is_object($criteria) ? print_r($criteria->sql, true)."\n".print_r($criteria->bindings, true) : 'no criteria'));
2191 }
2192 return $collection;
2193 }
2194
2195 /**
2196 * Initializes the field names with the qualified table name.
2197 *
2198 * Once this is called, you can lookup the qualified name by the field name
2199 * itself in {@link xPDOObject::$fieldNames}.
2200 *
2201 * @access protected
2202 */
2203 protected function _initFields() {
2204 reset($this->_fieldMeta);
2205 while (list ($k, $v)= each($this->_fieldMeta)) {
2206 $this->fieldNames[$k]= $this->xpdo->escape($this->_table) . '.' . $this->xpdo->escape($k);
2207 }
2208 }
2209
2210 /**
2211 * Returns a JSON representation of the object.
2212 *
2213 * @param string $keyPrefix An optional prefix to prepend to the field keys.
2214 * @param boolean $rawValues An optional flag indicating if the field values
2215 * should be returned raw or via {@link xPDOObject::get()}.
2216 * @return string A JSON string representing the object.
2217 */
2218 public function toJSON($keyPrefix= '', $rawValues= false) {
2219 $json= '';
2220 $array= $this->toArray($keyPrefix, $rawValues);
2221 if ($array) {
2222 $json= $this->xpdo->toJSON($array);
2223 }
2224 return $json;
2225 }
2226
2227 /**
2228 * Sets the object fields from a JSON object string.
2229 *
2230 * @param string $jsonSource A JSON object string.
2231 * @param string $keyPrefix An optional prefix to strip from the keys.
2232 * @param boolean $setPrimaryKeys Indicates if primary key fields should be set.
2233 * @param boolean $rawValues Indicates if values should be set raw or via
2234 * {@link xPDOObject::set()}.
2235 * @param boolean $adhocValues Indicates if ad hoc fields should be added to the
2236 * xPDOObject from the source object.
2237 */
2238 public function fromJSON($jsonSource, $keyPrefix= '', $setPrimaryKeys= false, $rawValues= false, $adhocValues= false) {
2239 $array= $this->xpdo->fromJSON($jsonSource, true);
2240 if ($array) {
2241 $this->fromArray($array, $keyPrefix, $setPrimaryKeys, $rawValues, $adhocValues);
2242 } else {
2243 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::fromJSON() -- Could not convert jsonSource to a PHP array.');
2244 }
2245 }
2246
2247 /**
2248 * Encodes a string using the specified algorithm.
2249 *
2250 * NOTE: This implementation currently only implements md5. To implement additional
2251 * algorithms, override this function in your xPDOObject derivative classes.
2252 *
2253 * @param string $source The string source to encode.
2254 * @param string $type The type of encoding algorithm to apply, md5 by default.
2255 * @return string The encoded string.
2256 */
2257 public function encode($source, $type= 'md5') {
2258 if (!is_string($source) || empty ($source)) {
2259 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::encode() -- Attempt to encode source data that is not a string (or is empty); encoding skipped.');
2260 return $source;
2261 }
2262 switch ($type) {
2263 case 'password':
2264 case 'md5':
2265 $encoded= md5($source);
2266 break;
2267 default :
2268 $encoded= $source;
2269 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::encode() -- Attempt to encode source data using an unsupported encoding algorithm ({$type}).");
2270 break;
2271 }
2272 return $encoded;
2273 }
2274
2275 /**
2276 * Indicates if an object field has been modified (or never saved).
2277 *
2278 * @access public
2279 * @param string $key The field name to check.
2280 * @return boolean True if the field exists and either has been modified or the object is new.
2281 */
2282 public function isDirty($key) {
2283 $dirty= false;
2284 $actualKey = $this->getField($key, true);
2285 if ($actualKey !== false) {
2286 if (array_key_exists($actualKey, $this->_dirty) || $this->isNew()) {
2287 $dirty= true;
2288 }
2289 } else {
2290 $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::isDirty() -- Attempt to check if an unknown field ({$key}) has been modified.");
2291 }
2292 return $dirty;
2293 }
2294
2295 /**
2296 * Add the field to a collection of field keys that have been modified.
2297 *
2298 * This function also clears any validation flag associated with the field.
2299 *
2300 * @param string $key The key of the field to set dirty.
2301 */
2302 public function setDirty($key= '') {
2303 if (empty($key)) {
2304 foreach (array_keys($this->_fieldMeta) as $fIdx => $fieldKey) {
2305 $this->setDirty($fieldKey);
2306 }
2307 }
2308 else {
2309 $key = $this->getField($key, true);
2310 if ($key !== false) {
2311 $this->_dirty[$key] = $key;
2312 if (isset($this->_validated[$key])) unset($this->_validated[$key]);
2313 }
2314 }
2315 }
2316
2317 /**
2318 * Indicates if the instance is new, and has not yet been persisted.
2319 *
2320 * @return boolean True if the object has not been saved or was loaded from
2321 * the database.
2322 */
2323 public function isNew() {
2324 return (boolean) $this->_new;
2325 }
2326
2327 /**
2328 * Gets the database data type for the specified field.
2329 *
2330 * @access protected
2331 * @param string $key The field name to get the data type for.
2332 * @return string The DB data type of the field.
2333 */
2334 protected function _getDataType($key) {
2335 $type= 'text';
2336 $actualKey = $this->getField($key, true);
2337 if ($actualKey !== false && isset($this->_fieldMeta[$actualKey]['dbtype'])) {
2338 $type= strtolower($this->_fieldMeta[$actualKey]['dbtype']);
2339 } elseif ($this->xpdo->getDebug() === true) {
2340 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "xPDOObject::_getDataType() -- No data type specified for field ({$key}), using `text`.");
2341 }
2342 return $type;
2343 }
2344
2345 /**
2346 * Gets the php data type for the specified field.
2347 *
2348 * @access protected
2349 * @param string $key The field name to get the data type for.
2350 * @return string The PHP data type of the field.
2351 */
2352 protected function _getPHPType($key) {
2353 $type= 'string';
2354 $actualKey = $this->getField($key, true);
2355 if ($actualKey !== false && isset($this->_fieldMeta[$actualKey]['phptype'])) {
2356 $type= strtolower($this->_fieldMeta[$actualKey]['phptype']);
2357 } elseif ($this->xpdo->getDebug() === true) {
2358 $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "xPDOObject::_getPHPType() -- No PHP type specified for field ({$key}), using `string`.");
2359 }
2360 return $type;
2361 }
2362
2363 /**
2364 * Load persistent data from the source for the field(s) indicated.
2365 *
2366 * @access protected
2367 * @param string|array $fields A field name or array of field names to load
2368 * from the data source.
2369 */
2370 protected function _loadFieldData($fields) {
2371 if (!is_array($fields)) $fields= array($fields);
2372 else $fields= array_values($fields);
2373 $criteria= $this->xpdo->newQuery($this->_class, $this->getPrimaryKey());
2374 $criteria->select($fields);
2375 if ($rows= xPDOObject :: _loadRows($this->xpdo, $this->_class, $criteria)) {
2376 $row= $rows->fetch(PDO::FETCH_ASSOC);
2377 $rows->closeCursor();
2378 $this->fromArray($row, '', false, true);
2379 $this->_lazy= array_diff($this->_lazy, $fields);
2380 }
2381 }
2382
2383 /**
2384 * Set a raw value on a field converted to the appropriate type.
2385 *
2386 * @access protected
2387 * @param string $key The key identifying the field to set.
2388 * @param mixed $val The value to set.
2389 * @return boolean Returns true if the value was set, false otherwise.
2390 */
2391 protected function _setRaw($key, $val) {
2392 $set = false;
2393 if ($val === null) {
2394 $this->_fields[$key] = null;
2395 $set = true;
2396 } else {
2397 $phptype = $this->_getPHPType($key);
2398 $dbtype = $this->_getDataType($key);
2399 switch ($phptype) {
2400 case 'int':
2401 case 'integer':
2402 case 'boolean':
2403 $this->_fields[$key] = (integer) $val;
2404 $set = true;
2405 break;
2406 case 'float':
2407 $this->_fields[$key] = (float) $val;
2408 $set = true;
2409 break;
2410 case 'array':
2411 if (is_array($val)) {
2412 $this->_fields[$key]= serialize($val);
2413 $set = true;
2414 } elseif (is_string($val)) {
2415 $this->_fields[$key]= $val;
2416 $set = true;
2417 } elseif (is_object($val) && $val instanceof xPDOObject) {
2418 $this->_fields[$key]= serialize($val->toArray());
2419 $set = true;
2420 }
2421 break;
2422 case 'json':
2423 if (!is_string($val)) {
2424 $v = $val;
2425 if (is_array($v)) {
2426 $this->_fields[$key] = $this->xpdo->toJSON($v);
2427 $set = true;
2428 } elseif (is_object($v) && $v instanceof xPDOObject) {
2429 $this->_fields[$key] = $this->xpdo->toJSON($v->toArray());
2430 $set = true;
2431 }
2432 } else {
2433 $this->_fields[$key]= $val;
2434 $set = true;
2435 }
2436 break;
2437 case 'date':
2438 case 'datetime':
2439 case 'timestamp':
2440 if (preg_match('/int/i', $dbtype)) {
2441 $this->_fields[$key] = (integer) $val;
2442 $set = true;
2443 break;
2444 }
2445 default:
2446 $this->_fields[$key] = $val;
2447 $set = true;
2448 }
2449 }
2450 if ($set) $this->setDirty($key);
2451 return $set;
2452 }
2453
2454 /**
2455 * Find aliases for any defined object relations of the specified class.
2456 *
2457 * @access protected
2458 * @param string $class The name of the class to find aliases from.
2459 * @param int $limit An optional limit on the number of aliases to return;
2460 * default is 0, i.e. no limit.
2461 * @return array An array of aliases or an empty array if none are found.
2462 */
2463 protected function _getAliases($class, $limit = 0) {
2464 $aliases = array();
2465 $limit = intval($limit);
2466 $array = array('aggregates' => $this->_aggregates, 'composites' => $this->_composites);
2467 foreach ($array as $relType => $relations) {
2468 foreach ($relations as $alias => $def) {
2469 if (isset($def['class']) && $def['class'] == $class) {
2470 $aliases[] = $alias;
2471 if ($limit > 0 && count($aliases) > $limit) break;
2472 }
2473 }
2474 }
2475 return $aliases;
2476 }
2477}
2478
2479/**
2480 * Extend to define a class with a native integer primary key field named id.
2481 *
2482 * @see xpdo/om/mysql/xpdosimpleobject.map.inc.php
2483 * @package xpdo
2484 * @subpackage om
2485 */
2486class xPDOSimpleObject extends xPDOObject {}