PageRenderTime 54ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 1ms

/core/xpdo/om/xpdoobject.class.php

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