PageRenderTime 26ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/phocoa/framework/generator/WFFixture.php

https://github.com/SwissalpS/phocoa
PHP | 364 lines | 246 code | 15 blank | 103 comment | 31 complexity | 7351d5f4db9c7e62c315078ccae2acf0 MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * vim: set autoindent expandtab tabstop=4 shiftwidth=4 :
  4. */
  5. /**
  6. * YAML Fixtures Data loader utility.
  7. *
  8. * The YAML file format supports a variety of ways to create objects for maximal flexibility.
  9. *
  10. * You can create objects of any kind (as long as they are KVC-Compliant) and set values of those objects.
  11. * You can even create related objects, either inline, or by referencing "tagged" objects that were created previously.
  12. * You can also use actual PHP code to execute arbitrary instructions in the fixture process. This is useful for using const's, looking up data in databases, or randomizing data.
  13. *
  14. * <code>
  15. * Basic Structure
  16. * <className>:
  17. * - New anonymous object
  18. * <tag>: new Named object
  19. *
  20. * Example
  21. * MyClass:
  22. * - prop1: val1
  23. * prop2: val2
  24. * - prop1: val3
  25. * prop2: val4
  26. * MyOtherClass:
  27. * inst1:
  28. * prop1: val1
  29. * prop2: val2
  30. * MyRelationshipName:
  31. * prop1: a
  32. * prop2: <?php MyRelatedClass::SOME_CONSTANT ?>
  33. * MyToOneRelatedClass:
  34. * prop1: val1
  35. * myOtherClass: inst1
  36. * MyToManyRelatedClass:
  37. * - prop1: val1
  38. * myOtherClass: inst1
  39. * - prop1: val2
  40. * myOtherClass: inst1
  41. * </code>
  42. *
  43. * Usage:
  44. *
  45. * $rootObjects = WFFixture::WFFixture()->loadFile('myData.yaml');
  46. * $rootObjects = WFFixture::WFFixture()->loadFile('myData.yaml', 'save'); // call "save()" method on all created objects (at root level)
  47. * @todo Refactor with Factory pattern instead of just methods on the class for various source structures.
  48. */
  49. class WFFixture extends WFObject
  50. {
  51. protected $objById = array();
  52. protected $options = array();
  53. const OPT_MODEL_ADAPTER = 'modelAdapter';
  54. public function __construct($opts = array())
  55. {
  56. parent::__construct();
  57. $this->options = array_merge(array(
  58. self::OPT_MODEL_ADAPTER => 'Propel',
  59. ), $opts);
  60. }
  61. public function getOpt($opt)
  62. {
  63. if (!isset($this->options[$opt])) throw new Exception("Unknown option: {$opt}.");
  64. return $this->options[$opt];
  65. }
  66. /**
  67. * Fluent constructor.
  68. *
  69. * @return object WFFixture
  70. */
  71. public static function WFFixture($opts = array())
  72. {
  73. return new WFFixture($opts);
  74. }
  75. /**
  76. * Function to load data structures via fixtures. Fixtures are YAML files that specify name-value pairs for objects.
  77. *
  78. * @param array An array of paths to YAML fixture files.
  79. * @param string The method to call on all top-level objects declared in the YAML file. Defaults to NULL (no call). Useful for calling "save" method to persist fixtures to a DB.
  80. * @param array An array of arguments to pass into the save method. Useful for Dependency Injection of db connection for instance.
  81. * @return array An array containing all created objects, as an associative array. NOTE that if you load multiple files with this call and you have the same "name" for different objects,
  82. * they will stomp each other and you aren't guaranteed which one you'll get.
  83. * @throws object WFException
  84. */
  85. public function loadFiles($files, $saveMethod = NULL, $saveMethodArgs = array())
  86. {
  87. $allCreatedObjects = array();
  88. // load the fixtures data & process
  89. foreach ($files as $file)
  90. {
  91. $pathParts = pathinfo($file);
  92. switch (strtolower($pathParts['extension'])) {
  93. case 'yaml':
  94. $newObjs = $this->processObjectList(WFYaml::load($file));
  95. $allCreatedObjects = array_merge($newObjs, $allCreatedObjects);
  96. break;
  97. default:
  98. throw( new WFException("No fixture support for files of type {$pathParts['extension']}.") );
  99. }
  100. if ($saveMethod)
  101. {
  102. foreach ($newObjs as $o)
  103. {
  104. try {
  105. call_user_func_array(array($o, $saveMethod), $saveMethodArgs);
  106. } catch (Exception $e) {
  107. throw (new WFException("Uncaught Exception (" . get_class($e) . ") saving object: " . $o . "\n" . $e->getMessage()) );
  108. }
  109. }
  110. }
  111. }
  112. return $allCreatedObjects;
  113. }
  114. /**
  115. * Function to load data structures via fixtures. Fixtures are YAML files that specify name-value pairs for objects.
  116. *
  117. * @param string The yaml file path.
  118. * @param string The method to call on all top-level objects declared in the YAML file. Defaults to NULL (no call). Useful for calling "save" method to persist fixtures to a DB.
  119. * @param array An array of arguments to pass into the save method. Useful for Dependency Injection of db connection for instance.
  120. * @return array An array containing all created objects, as an associative array.
  121. * @throws object WFException
  122. */
  123. public function loadFile($file, $saveMethod = NULL, $saveMethodArgs = array())
  124. {
  125. return WFFixture::loadFiles(array($file), $saveMethod, $saveMethodArgs);
  126. }
  127. public function loadFromYaml($yamlString, $saveMethod = NULL)
  128. {
  129. // load the fixtures data & process
  130. $allCreatedObjects = $this->processObjectList(WFYaml::loadString($yamlString));
  131. if ($saveMethod)
  132. {
  133. foreach ($allCreatedObjects as $o)
  134. {
  135. try {
  136. $o->$saveMethod();
  137. } catch (Exception $e) {
  138. throw (new WFException("Error saving object: " . $o . "\n" . $e->getMessage()) );
  139. }
  140. }
  141. }
  142. return $allCreatedObjects;
  143. }
  144. /**
  145. * Creates objects from the passed list. The list should be a hash where the keys are class names and the values are arrays of sets of property values.
  146. *
  147. * @param array
  148. * @return array
  149. * @throws WFException
  150. */
  151. protected function processObjectList($list)
  152. {
  153. $allCreatedObjects = array();
  154. foreach ($list as $class_name => $instances)
  155. {
  156. if (!is_array($instances))
  157. {
  158. throw(new WFException("Class definition for {$class_name} doesn't have any instances."));
  159. }
  160. foreach ($instances as $instanceId => $props)
  161. {
  162. try {
  163. $o = $this->makeObj($class_name, $props);
  164. } catch (Exception $e) {
  165. throw( new WFException("Error processing class {$class_name}[{$instanceId}]: " . $e->getMessage() . $e->getTraceAsString()) );
  166. }
  167. // store all "named" objects for future reference
  168. if (gettype($instanceId) != 'integer')
  169. {
  170. if (isset($this->objById[$class_name][$instanceId])) throw( new Exception("There already exists a {$class_name} for id {$instanceId}.") );
  171. $this->objById[$class_name][$instanceId] = $o;
  172. }
  173. // store all created objects; save them as named objects if possible
  174. if (gettype($instanceId) == 'integer')
  175. {
  176. $allCreatedObjects[] = $o;
  177. }
  178. else
  179. {
  180. if (isset($allCreatedObjects[$instanceId])) throw( new WFException("Can't have two objects with the same instance ID in the same object list.") );
  181. $allCreatedObjects[$instanceId] = $o;
  182. }
  183. }
  184. }
  185. return $allCreatedObjects;
  186. }
  187. /**
  188. * Create an object of Class with the passed name/value pairs.
  189. *
  190. * @param string The class of object to create.
  191. * @return object The instance created.
  192. * @throws object WFException
  193. */
  194. protected function makeObj($class, $props)
  195. {
  196. if (!is_array($props))
  197. {
  198. throw(new WFException("Class definition for {$class} doesn't have any properties. Properties must be defined in an array."));
  199. }
  200. $model = WFModel::sharedModel();
  201. $o = new $class;
  202. if ($o instanceof BaseObject)
  203. {
  204. // model object; could have relationships, see if we need to build it...
  205. if (!$model->getEntity($class))
  206. {
  207. $model->buildModel($this->getOpt(self::OPT_MODEL_ADAPTER), NULL, array($class));
  208. }
  209. foreach ($props as $k => $v)
  210. {
  211. // is this an attribute or a relationship?
  212. $entity = $model->getEntity($class);
  213. if ($entity->getProperty($k))
  214. {
  215. // it's a basic property
  216. if (!is_array($v))
  217. {
  218. // is $v a php eval?
  219. $matches = array();
  220. if (preg_match('/<\?php (.*)\?'.'>/', $v, $matches)) // goofy syntax there to prevent syntax coloring problems in rest of file due to close php tag
  221. {
  222. $v = eval( "return {$matches[1]};" );
  223. }
  224. $o->setValueForKey($v, $k);
  225. }
  226. else
  227. {
  228. // "value" for this key is an object.
  229. if (count(array_keys($v)) != 1) throw( new WFException("Fixtures can pass only scalars or objects. Arrays of object are not supported.") );
  230. $subClass = key($v);
  231. $subClassProps = $v[$subClass];
  232. $subObj = $this->makeObj($subClass, $subClassProps);
  233. $o->setValueForKey($subObj, $k);
  234. }
  235. }
  236. else
  237. {
  238. // not a property, it is a relationship...
  239. // see if we can get that relationship
  240. if (!$model->getEntity($k))
  241. {
  242. // try to build the entity; can't get all relationships unless we build the related entity
  243. try {
  244. $model->buildModel($this->getOpt(self::OPT_MODEL_ADAPTER), NULL, array($k));
  245. } catch (Exception $e) {
  246. // maybe it's an instance variable or just a KVC property?
  247. try {
  248. // is $v a php eval?
  249. $matches = array();
  250. if (preg_match('/<\?php (.*)\?'.'>/', $v, $matches))
  251. {
  252. $v = eval( "return {$matches[1]};" );
  253. }
  254. $o->setValueForKey($v, $k);
  255. continue;
  256. } catch (Exception $e) {
  257. throw( new WFException("{$k} is not a known Propel entity, nor is it a KVC method:" . $e->getMessage()) );
  258. }
  259. }
  260. }
  261. $rel = $entity->getRelationship($k);
  262. if (!$rel)
  263. {
  264. // maybe it's an instance variable or just a KVC property?
  265. try {
  266. // is $v a php eval?
  267. $matches = array();
  268. if (preg_match('/<\?php (.*)\?'.'>/', $v, $matches))
  269. {
  270. $v = eval( "return {$matches[1]};" );
  271. }
  272. $o->setValueForKey($v, $k);
  273. continue;
  274. } catch (Exception $e) {
  275. throw( new WFException("{$k} is not a manifested entity relationship or a KVC-compliant property of {$class}.") );
  276. }
  277. }
  278. if ($rel->valueForKey('toOne'))
  279. {
  280. // check for "empty" object
  281. if ($v === NULL)
  282. {
  283. $v = array();
  284. }
  285. if (is_array($v))
  286. {
  287. // treat the obj as an "inline object"
  288. $subObj = $this->makeObj($k, $v);
  289. }
  290. else
  291. {
  292. // we expect an object here. this could be either a named object or result of an eval
  293. // is $v a php eval?
  294. $matches = array();
  295. if (preg_match('/<\?php (.*)\?>/', $v, $matches))
  296. {
  297. $subObj = eval( "return {$matches[1]};" );
  298. if (!is_object($subObj)) throw( new WFException("Result of eval for relationship {$v} is not an object: " . $matches[1]) );
  299. }
  300. else // we expect a label to a YAML object elsewhere
  301. {
  302. if (!isset($this->objById[$k][$v])) throw( new WFException("No {$k} with label {$v}.") );
  303. $subObj = $this->objById[$k][$v];
  304. }
  305. }
  306. $o->setValueForKey($subObj, $k);
  307. }
  308. else
  309. {
  310. foreach ($v as $index => $subClassProps) {
  311. try {
  312. // we expect an object here. this could be either a named object or result of an "inline" object via YAML
  313. // if subClassProps is a string, it's the former.
  314. if (is_string($subClassProps)) // we expect a label to a YAML object elsewhere
  315. {
  316. if (!isset($this->objById[$k][$subClassProps])) throw( new WFException("Failed to find a named object of class {$k} with label {$subClassProps}.") );
  317. $subObj = $this->objById[$k][$subClassProps];
  318. }
  319. else
  320. {
  321. $subObj = $this->makeObj($k, $v[$index]);
  322. }
  323. } catch (Exception $e) {
  324. throw( new WFException("Error processing {$index} class={$k}: " . $e->getMessage()) );
  325. }
  326. $toManyAdderF = "add" . $k;
  327. $o->$toManyAdderF($subObj);
  328. if (gettype($index) != 'integer')
  329. {
  330. if (isset($this->objById[$k][$index])) throw( new Exception("There already exists a {$k} for id {$index}.") );
  331. $this->objById[$k][$index] = $subObj;
  332. }
  333. }
  334. }
  335. }
  336. }
  337. }
  338. else
  339. {
  340. // simple object; has to contain key-value pairs.
  341. foreach ($props as $k => $v)
  342. {
  343. $o->setValueForKey($v, $k);
  344. }
  345. }
  346. return $o;
  347. }
  348. }