xpdoobject.class.php 113 KB

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