PageRenderTime 49ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/app/plugins/Db/libraries/Atomik/Model/Builder/ClassMetadata.php

https://bitbucket.org/fbertagnin/fbwork4
PHP | 360 lines | 205 code | 50 blank | 105 comment | 55 complexity | 13d39710b197e5767f9ff2add6e2bb06 MD5 | raw file
  1. <?php
  2. /**
  3. * Atomik Framework
  4. * Copyright (c) 2008-2009 Maxime Bouroumeau-Fuseau
  5. *
  6. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  7. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  8. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  9. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  10. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  11. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  12. * THE SOFTWARE.
  13. *
  14. * @package Atomik
  15. * @subpackage Model
  16. * @author Maxime Bouroumeau-Fuseau
  17. * @copyright 2008-2009 (c) Maxime Bouroumeau-Fuseau
  18. * @license http://www.opensource.org/licenses/mit-license.php
  19. * @link http://www.atomikframework.com
  20. */
  21. /** Atomik_Model_Builder */
  22. require_once 'Atomik/Model/Builder.php';
  23. /** Atomik_Model_Builder_Factory */
  24. require_once 'Atomik/Model/Builder/Factory.php';
  25. /**
  26. * Reads the doc comments of a class and its properties and generates a model builder
  27. *
  28. * @package Atomik
  29. * @subpackage Model
  30. */
  31. class Atomik_Model_Builder_ClassMetadata
  32. {
  33. /**
  34. * @var array
  35. */
  36. private static $_cache = array();
  37. /**
  38. * Reads metadata from a class doc comments and creates a builder object
  39. *
  40. * @param string $className
  41. * @return Atomik_Model_Builder
  42. */
  43. public static function read($className)
  44. {
  45. $builder = self::_getBaseBuilder($className);
  46. // extract references
  47. $references = array();
  48. if ($builder->hasOption('has')) {
  49. $references = (array) $builder->getOption('has');
  50. $builder->removeOption('has');
  51. }
  52. // adds references
  53. foreach ($references as $referenceString) {
  54. self::addReferenceFromString($builder, $referenceString);
  55. }
  56. // extract links
  57. $links = array();
  58. if ($builder->hasOption('link-to')) {
  59. $links = (array) $builder->getOption('link-to');
  60. $builder->removeOption('link-to');
  61. }
  62. // adds links
  63. foreach ($links as $linkString) {
  64. self::addLinkFromString($builder, $linkString);
  65. }
  66. // extract behaviours
  67. $behaviours = array();
  68. if ($builder->hasOption('act-as')) {
  69. $behaviours = (array) $builder->getOption('act-as');
  70. $builder->removeOption('act-as');
  71. }
  72. // adds behaviours
  73. foreach ($behaviours as $behaviourString) {
  74. foreach (explode(',', $behaviourString) as $behaviour) {
  75. $builder->getBehaviourBroker()->addBehaviour(
  76. Atomik_Model_Behaviour_Factory::factory(trim($behaviour)));
  77. }
  78. }
  79. return $builder;
  80. }
  81. /**
  82. * Returns a builder without the references
  83. *
  84. * @param string $className
  85. * @return Atomik_Model_Builder
  86. */
  87. private static function _getBaseBuilder($className)
  88. {
  89. if (isset(self::$_cache[$className])) {
  90. return self::$_cache[$className];
  91. }
  92. if (!class_exists($className)) {
  93. require_once 'Atomik/Model/Builder/Exception.php';
  94. throw new Atomik_Model_Builder_Exception('Class ' . $className . ' not found');
  95. }
  96. $class = new ReflectionClass($className);
  97. $builder = new Atomik_Model_Builder($className, $className);
  98. foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
  99. $propData = self::getMetadataFromDocBlock($prop->getDocComment());
  100. // jump to the next property if there is the ignore tag
  101. if (isset($propData['ignore'])) {
  102. continue;
  103. }
  104. $type = 'string';
  105. if (isset($propData['var'])) {
  106. $type = $propData['var'];
  107. unset($propData['var']);
  108. } else if (!isset($propData['length'])) {
  109. $propData['length'] = 255;
  110. }
  111. $field = Atomik_Model_Field_Factory::factory($type, $prop->getName(), $propData);
  112. $builder->addField($field);
  113. }
  114. $options = self::getMetadataFromDocBlock($class->getDocComment());
  115. // sets the adapter
  116. if (isset($options['table'])) {
  117. $builder->tableName = $options['table'];
  118. unset($options['table']);
  119. }
  120. // use the remaining metadatas as options
  121. $builder->setOptions($options);
  122. if (($parentClass = $class->getParentClass()) != null) {
  123. if ($parentClass->getName() != 'Atomik_Model' && $parentClass->isSubclassOf('Atomik_Model')) {
  124. $builder->setParentModel($parentClass->getName());
  125. }
  126. }
  127. self::$_cache[$className] = $builder;
  128. return $builder;
  129. }
  130. /**
  131. * Retreives metadata tags (i.e. the one starting with @) from a doc block
  132. *
  133. * @param string $doc
  134. * @return array
  135. */
  136. public static function getMetadataFromDocBlock($doc)
  137. {
  138. $metadata = array();
  139. preg_match_all('/@(.+)$/mU', $doc, $matches);
  140. for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
  141. if (($separator = strpos($matches[1][$i], ' ')) !== false) {
  142. $key = trim(substr($matches[1][$i], 0, $separator));
  143. $value = trim(substr($matches[1][$i], $separator + 1));
  144. // boolean
  145. if ($value == 'false') {
  146. $value = false;
  147. } else if ($value == 'true') {
  148. $value = true;
  149. }
  150. } else {
  151. $key = trim($matches[1][$i]);
  152. $value = true;
  153. }
  154. if (isset($metadata[$key])) {
  155. if (!is_array($metadata[$key])) {
  156. $metadata[$key] = array($metadata[$key]);
  157. }
  158. $metadata[$key][] = $value;
  159. } else {
  160. $metadata[$key] = $value;
  161. }
  162. }
  163. return $metadata;
  164. }
  165. /**
  166. * Builds a reference object from a string and adds it to the builder. The string should follow the following pattern:
  167. * (one|many) foreignModel [as property] [using localModel.localField = foreignModel.foreignField] [order by field] [limit offset, length]
  168. *
  169. * @param Atomik_Model_Builder $builder
  170. * @param string $string
  171. * @return Atomik_Model_Builder_Reference
  172. */
  173. public static function addReferenceFromString(Atomik_Model_Builder $builder, $string)
  174. {
  175. $regexp = '/(?P<type>one|many|parent)\s+(?P<target>.+)((\sas\s(?P<as>.+))|)((\svia\s(?P<viatype>table|model|)\s(?P<via>.+))|)'
  176. . '((\susing\s(?P<using>.+))|)((\sorder by\s(?P<order>.+))|)((\slimit\s(?P<limit>.+))|)$/U';
  177. if (!preg_match($regexp, $string, $matches)) {
  178. require_once 'Atomik/Model/Builder/Exception.php';
  179. throw new Atomik_Model_Builder_Exception('Reference string is malformed: ' . $string);
  180. }
  181. // type and target
  182. $type = $matches['type'];
  183. $target = trim($matches['target']);
  184. // name
  185. $name = $target;
  186. if (isset($matches['as']) && !empty($matches['as'])) {
  187. $name = trim($matches['as']);
  188. }
  189. $reference = new Atomik_Model_Builder_Reference($name, $type);
  190. // via
  191. if (isset($matches['via']) && !empty($matches['via'])) {
  192. // @TODO support via in model references
  193. }
  194. // fields
  195. if (isset($matches['using']) && !empty($matches['using'])) {
  196. list($sourceField, $targetField) = self::getReferenceFieldsFromString($matches['using'], $builder->name);
  197. } else {
  198. list($sourceField, $targetField) = self::getReferenceFields($builder, $target, $type);
  199. }
  200. $reference->sourceField = $sourceField;
  201. $reference->targetField = $targetField;
  202. // order by
  203. if (isset($matches['order']) && !empty($matches['order'])) {
  204. $reference->query->orderBy($matches['order']);
  205. }
  206. // limit
  207. if (isset($matches['limit']) && !empty($matches['limit'])) {
  208. $reference->query->limit($matches['limit']);
  209. }
  210. $reference->target = $target;
  211. $builder->addReference($reference);
  212. return $reference;
  213. }
  214. /**
  215. * Returns reference fields depending on the type of reference
  216. *
  217. * @param Atomik_Model_Builder $builder
  218. * @param string $targetName
  219. * @param string $type
  220. * @return array array(sourceField, targetField)
  221. */
  222. public static function getReferenceFields(Atomik_Model_Builder $builder, $targetName, $type)
  223. {
  224. $targetBuilder = self::_getBaseBuilder($targetName);
  225. if ($type == Atomik_Model_Builder_Reference::HAS_PARENT) {
  226. // targetModel.targetPrimaryKey = sourceModel.targetModel_targetPrimaryKey
  227. $targetField = $targetBuilder->getPrimaryKeyField()->name;
  228. $sourceField = strtolower($targetName) . '_' . $targetField;
  229. } else if ($type == Atomik_Model_Builder_Reference::HAS_ONE) {
  230. // targetModel.sourceModel_sourcePrimaryKey = sourceModel.sourcePrimaryKey
  231. $sourceField = $builder->getPrimaryKeyField()->name;
  232. $targetField = strtolower($builder->name) . '_' . $sourceField;
  233. } else {
  234. $targetBuilder = Atomik_Model_Builder_Factory::get($targetName);
  235. // HAS_MANY
  236. // searching through the target model references for one pointing back to this model
  237. $parentRefs = $targetBuilder->getReferences();
  238. $found = false;
  239. foreach ($parentRefs as $parentRef) {
  240. if ($parentRef->isHasParent() && $parentRef->isTarget($builder->name)) {
  241. $sourceField = $parentRef->targetField;
  242. $targetField = $parentRef->sourceField;
  243. $found = true;
  244. break;
  245. }
  246. }
  247. if (!$found) {
  248. require_once 'Atomik/Model/Builder/Exception.php';
  249. throw new Atomik_Model_Builder_Exception('No back reference in ' . $targetName . ' for ' . $builder->name);
  250. }
  251. }
  252. return array($sourceField, $targetField);
  253. }
  254. /**
  255. * Returns reference fields computed from a string which should follow
  256. * the pattern sourceMode.sourceField = targetModel.targetField (or vice versa)
  257. *
  258. * @param string $string
  259. * @param string $sourceName
  260. * @return array array(sourceField, targetField)
  261. */
  262. public static function getReferenceFieldsFromString($string, $sourceName)
  263. {
  264. if (!preg_match('/(.+)\.(.+)\s(=)\s(.+)\.(.+)/', $string, $matches)) {
  265. require_once 'Atomik/Model/Builder/Exception.php';
  266. throw new Atomik_Model_Builder_Exception('Using statement for reference is malformed: ' . $string);
  267. }
  268. if ($matches[1] == $sourceName) {
  269. return array($matches[2], $matches[5]);
  270. }
  271. return array($matches[5], $matches[2]);
  272. }
  273. /**
  274. * Builds a link from the string and adds it to the builder
  275. *
  276. * String template:
  277. * [one] Target [as property] on field [=> alias] [, field [=> alias]]
  278. *
  279. * Example:
  280. * Tweets as tweets on twitter_username => username
  281. *
  282. * @param Atomik_Model_Builder $builder
  283. * @param string $string
  284. */
  285. public static function addLinkFromString(Atomik_Model_Builder $builder, $string)
  286. {
  287. $regexp = '/^((?P<type>one)\s+|)(?P<target>.+)((\sas\s(?P<as>.+))|)\s+on\s+(?P<on>.+)$/U';
  288. if (!preg_match($regexp, $string, $matches)) {
  289. require_once 'Atomik/Model/Builder/Exception.php';
  290. throw new Atomik_Model_Builder_Exception('Link string is malformed: ' . $string);
  291. }
  292. $link = new Atomik_Model_Builder_Link();
  293. $link->type = $matches['type'] == 'one' ? 'one' : 'many';
  294. $link->target = trim($matches['target']);
  295. $link->name = $link->target;
  296. if (isset($matches['as']) && !empty($matches['as'])) {
  297. $link->name = trim($matches['as']);
  298. }
  299. $fields = explode(',', $matches['on']);
  300. foreach ($fields as $field) {
  301. if (!preg_match('/^(?P<name>.+)(\s+=\>\s(?P<alias>.+)|)$/U', trim($field), $fieldMatches)) {
  302. require_once 'Atomik/Model/Builder/Exception.php';
  303. throw new Atomik_Model_Builder_Exception('Field definition in link string is malformed: ' . $string);
  304. }
  305. $alias = isset($fieldMatches['alias']) ? $fieldMatches['alias'] : $fieldMatches['name'];
  306. $link->fields[$alias] = $fieldMatches['name'];
  307. }
  308. $builder->addLink($link);
  309. }
  310. }