PageRenderTime 28ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/system/expressionengine/libraries/relationship_parser/Tree_builder.php

https://bitbucket.org/mbaily/tremain
PHP | 511 lines | 278 code | 87 blank | 146 comment | 42 complexity | bc2584c7afff16485a9072bc5632dc66 MD5 | raw file
  1. <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
  2. /**
  3. * ExpressionEngine - by EllisLab
  4. *
  5. * @package ExpressionEngine
  6. * @author EllisLab Dev Team
  7. * @copyright Copyright (c) 2003 - 2013, EllisLab, Inc.
  8. * @license http://ellislab.com/expressionengine/user-guide/license.html
  9. * @link http://ellislab.com
  10. * @since Version 2.6
  11. * @filesource
  12. */
  13. // ------------------------------------------------------------------------
  14. require_once APPPATH.'libraries/datastructures/Tree.php';
  15. require_once APPPATH.'libraries/relationship_parser/Nodes.php';
  16. require_once APPPATH.'libraries/relationship_parser/Iterators.php';
  17. // ------------------------------------------------------------------------
  18. /**
  19. * ExpressionEngine Tree Builder Class
  20. *
  21. * @package ExpressionEngine
  22. * @subpackage Core
  23. * @category Core
  24. * @author EllisLab Dev Team
  25. * @link http://ellislab.com
  26. */
  27. class EE_relationship_tree_builder {
  28. protected $_tree;
  29. protected $_unique_ids = array(); // all entry ids needed
  30. protected $relationship_field_ids = array(); // field_name => field_id
  31. protected $relationship_field_names = array(); // field_id => field_name
  32. /**
  33. * Create a tree builder for the given relationship fields
  34. */
  35. public function __construct(array $relationship_fields)
  36. {
  37. $this->relationship_field_ids = $relationship_fields;
  38. $this->relationship_field_names = array_flip($relationship_fields);
  39. }
  40. // --------------------------------------------------------------------
  41. /**
  42. * Find All Relationships of the Given Entries in the Template
  43. *
  44. * Searches the template the parser was constructed with for relationship
  45. * tags and then builds a tree of all the requested related entries for
  46. * each of the entries passed in the array.
  47. *
  48. * For space savings and subtree querying each node is pushed
  49. * its own set of entry ids per parent ids:
  50. *
  51. * {[6, 7]}
  52. * / \
  53. * {6:[2,4], 7:[8,9]} {6:[], 7:[2,5]}
  54. * / \
  55. * ... ...
  56. *
  57. * By pushing them down like this the subtree query is very simple.
  58. * And when we parse we simply go through all of them and make that
  59. * many copies of the node's tagdata.
  60. *
  61. * @param int[] An array of entry ids who's relations we need
  62. * to find.
  63. * @return object The tree root node
  64. */
  65. public function build_tree(array $entry_ids)
  66. {
  67. // first, we need a tag tree
  68. $root = $this->_build_tree();
  69. if ($root === NULL)
  70. {
  71. return NULL;
  72. }
  73. // not strictly necessary, but keeps all the id loops parent => children
  74. // it has no side-effects since all we really care about for the root
  75. // node are the children.
  76. foreach ($entry_ids as $id)
  77. {
  78. $root->add_entry_id((int)$id, (int)$id);
  79. }
  80. $all_entry_ids = array($entry_ids);
  81. $query_node_iterator = new RecursiveIteratorIterator(
  82. new QueryNodeIterator(array($root)),
  83. RecursiveIteratorIterator::SELF_FIRST
  84. );
  85. // For every query node we now run the query and push the ids
  86. // down onto their subtrees.
  87. foreach ($query_node_iterator as $node)
  88. {
  89. // the root uses the main entry ids, all others use all
  90. // of the parent's child ids. These form all of their potential
  91. // parents, and thus the where_in for our query.
  92. if ( ! $node->is_root())
  93. {
  94. $entry_ids = $node->parent()->entry_ids();
  95. $entry_ids = call_user_func_array('array_merge', $entry_ids);
  96. }
  97. // Store flattened ids for the big entry query
  98. $all_entry_ids[] = $this->_propagate_ids(
  99. $node,
  100. ee()->relationship_model->node_query($node, $entry_ids)
  101. );
  102. }
  103. $this->_unique_ids = array_unique(
  104. call_user_func_array('array_merge', $all_entry_ids),
  105. SORT_NUMERIC
  106. );
  107. return $root;
  108. }
  109. // --------------------------------------------------------------------
  110. /**
  111. * Create a parser from our collected tree.
  112. *
  113. * Runs the queries using our id information, builds lookup tables,
  114. * and finally stick it all onto an object that knows what to do with
  115. * it.
  116. *
  117. * @param object Root query node of the relationship tree
  118. * @return object The new relationships parser
  119. */
  120. public function get_parser(EE_TreeNode $root)
  121. {
  122. $unique_entry_ids = $this->_unique_ids;
  123. $category_lookup = array();
  124. $entries_result = array();
  125. if ( ! empty($unique_entry_ids))
  126. {
  127. // @todo reduce to only those that have a categories pair or parameter
  128. ee()->load->model('category_model');
  129. $category_lookup = ee()->category_model->get_entry_categories($unique_entry_ids);
  130. // ready set, main query.
  131. ee()->load->model('channel_entries_model');
  132. $entries_result = ee()->channel_entries_model->get_entry_data($unique_entry_ids);
  133. }
  134. // Build an id => data map for quick retrieval during parsing
  135. $entry_lookup = array();
  136. foreach ($entries_result as $entry)
  137. {
  138. $entry_lookup[$entry['entry_id']] = $entry;
  139. }
  140. if ( ! class_exists('EE_Relationship_data_parser'))
  141. {
  142. require_once APPPATH.'libraries/relationship_parser/Parser.php';
  143. }
  144. return new EE_Relationship_data_parser($root, $entry_lookup, $category_lookup);
  145. }
  146. // --------------------------------------------------------------------
  147. /**
  148. * Turn the tagdata hierarchy into a tree
  149. *
  150. * Looks through the tagdata string to find all of the relationship
  151. * tags that we might use and constructs a tree hierachy from them.
  152. *
  153. * @param array Entry ids
  154. * @return object Root node of the final tree
  155. */
  156. protected function _build_tree()
  157. {
  158. // extract the relationship tags straight from the channel
  159. // tagdata so that we can process it all in one fell swoop.
  160. $str = ee()->TMPL->tagdata;
  161. // No variables? No reason to continue...
  162. if (strpos($str, '{') === FALSE)
  163. {
  164. return NULL;
  165. }
  166. $all_fields = $this->relationship_field_names;
  167. $all_fields = implode('|', $all_fields).'|parents|siblings';
  168. // Regex to separate out the relationship prefix part from the rest
  169. // {rel:pre:fix:tag:modified param="value"}
  170. // 0 => full_match
  171. // 1 => rel:pre:fix:
  172. // 2 => tag:modified param="value"
  173. if ( ! preg_match_all("/".LD.'\/?((?:(?:'.$all_fields.'):?)+)\b([^}{]*)?'.RD."/", $str, $matches, PREG_SET_ORDER))
  174. {
  175. return NULL;
  176. }
  177. // nesting trackers
  178. // this code would probably be a little prettier with a state machine
  179. // instead of the crazy regex.
  180. $uuid = 1;
  181. $id_stack = array(0);
  182. $rel_stack = array();
  183. $root = new QueryNode('__root__');
  184. $nodes = array($root);
  185. foreach ($matches as $match)
  186. {
  187. $relationship_prefix = $match[1];
  188. // some helpful booleans
  189. $is_closing = ($match[0][1] == '/');
  190. $is_only_relationship = (substr($relationship_prefix, -1) != ':');
  191. // catch closing tags right away, we don't need them
  192. if ($is_closing)
  193. {
  194. // closing a relationship tag - pop the stacks
  195. if ($is_only_relationship)
  196. {
  197. array_pop($rel_stack);
  198. array_pop($id_stack);
  199. }
  200. continue;
  201. }
  202. $tag_name = rtrim($relationship_prefix, ':');
  203. // Opening tags are a little harder, it's a shortcut if it has
  204. // a non prefix portion and the prefix does not yet exist on the
  205. // stack. Otherwise it's a field we can safely skip.
  206. // Of course, if it has no tag, it's definitely a relationship
  207. // field and we have to track it.
  208. if ( ! $is_only_relationship && in_array($tag_name, $rel_stack))
  209. {
  210. continue;
  211. }
  212. list($tag, $parameters) = preg_split("/\s+/", $match[2].' ', 2);
  213. $parent_id = end($id_stack);
  214. // no closing tag tracking for shortcuts
  215. if ($is_only_relationship)
  216. {
  217. $id_stack[] = ++$uuid;
  218. $rel_stack[] = $tag_name;
  219. }
  220. // extract the full name and determining relationship
  221. $last_colon = strrpos($tag_name, ':');
  222. if ($last_colon === FALSE)
  223. {
  224. $determinant_relationship = $tag_name;
  225. }
  226. else
  227. {
  228. $determinant_relationship = substr($tag_name, $last_colon + 1);
  229. }
  230. // prep parameters
  231. $params = ee()->functions->assign_parameters($parameters);
  232. $params = $params ? $params : array();
  233. // setup node type
  234. // if it's a root sibling tag, or the determining relationship
  235. // is parents then we need to do a new query for them
  236. $node_class = 'ParseNode';
  237. if ($determinant_relationship == 'parents' OR $tag_name == 'siblings')
  238. {
  239. $node_class = 'QueryNode';
  240. }
  241. // instantiate and hook to tree
  242. $node = new $node_class($tag_name, array(
  243. 'field_name'=> $determinant_relationship,
  244. 'tag_info' => array(),
  245. 'entry_ids' => array(),
  246. 'params' => $params,
  247. 'shortcut' => $is_only_relationship ? FALSE : $tag,
  248. 'open_tag' => $match[0]
  249. ));
  250. if ($is_only_relationship)
  251. {
  252. $nodes[$uuid] = $node;
  253. }
  254. $parent = $nodes[$parent_id];
  255. $parent->add($node);
  256. }
  257. // Doing our own parsing let's us do error checking
  258. if (count($rel_stack))
  259. {
  260. throw new EE_Relationship_exception('Unmatched Relationship Tag: "{'.end($rel_stack).'}"');
  261. }
  262. return $root;
  263. }
  264. // --------------------------------------------------------------------
  265. /**
  266. * Push the id graph onto the tag graph.
  267. *
  268. * Given the possible ids of a query node and the leave paths of
  269. * all of its children, we can generate parent > children pairs
  270. * for all of the descendent parse nodes.
  271. *
  272. * @param object Root query node whose subtree to process
  273. * @param array Leave path array as created by _parse_leaves
  274. * @return array All unique entry ids processed.
  275. */
  276. protected function _propagate_ids(QueryNode $root, array $leave_paths)
  277. {
  278. $parse_node_iterator = new RecursiveIteratorIterator(
  279. new ParseNodeIterator(array($root)),
  280. RecursiveIteratorIterator::SELF_FIRST
  281. );
  282. $root_offset = 0;
  283. $all_entry_ids = array();
  284. $leaves = $this->_parse_leaves($leave_paths);
  285. foreach ($parse_node_iterator as $node)
  286. {
  287. $depth = $parse_node_iterator->getDepth();
  288. if ($node->is_root())
  289. {
  290. $root_offset = -1;
  291. continue;
  292. }
  293. $is_root_sibling = ($node->name() == 'siblings'); // unprefixed {sibling}
  294. // If the tag is prefixed:sibling, then we already have the ids
  295. // on the parent since our query is not limited in breadth.
  296. // This does not apply to an un-prefixed sibling tag which is
  297. // handled as regular subtree below.
  298. if ($node->field_name == 'siblings' && ! $is_root_sibling)
  299. {
  300. $siblings = array();
  301. $possible_siblings = $node->parent()->entry_ids();
  302. foreach ($possible_siblings as $parent => $children)
  303. {
  304. $children = array_unique($children);
  305. // find all sibling permutations
  306. for ($i = 0; $i < count($children); $i++)
  307. {
  308. $no_sibs = $children;
  309. list($key) = array_splice($no_sibs, $i, 1);
  310. $node->add_entry_id($key, $no_sibs);
  311. }
  312. }
  313. continue;
  314. }
  315. // the lookup below starts one up from the root
  316. $depth += $root_offset;
  317. $field_ids = NULL;
  318. // if the field contains parent or is siblings, we need to check
  319. // for the optional field= parameter.
  320. if ($node->field_name == 'parents' OR $node->field_name == 'siblings')
  321. {
  322. $field_ids = array();
  323. $field_name = $node->param('field');
  324. if ($field_name)
  325. {
  326. foreach (explode('|', $field_name) as $name)
  327. {
  328. $field_ids[] = $this->relationship_field_ids[$name];
  329. }
  330. }
  331. else
  332. {
  333. // no parameter, everything is fair game
  334. $field_ids = array_keys($leaves[$depth]);
  335. }
  336. }
  337. else
  338. {
  339. $field_ids = array(
  340. $this->relationship_field_ids[$node->field_name]
  341. );
  342. }
  343. // propogate the ids
  344. foreach ($field_ids as $field_id)
  345. {
  346. if (isset($leaves[$depth][$field_id]))
  347. {
  348. foreach ($leaves[$depth][$field_id] as $parent => $children)
  349. {
  350. foreach ($children as $child)
  351. {
  352. $child_id = $child['id'];
  353. if ($is_root_sibling && $parent == $child_id)
  354. {
  355. continue;
  356. }
  357. $node->add_entry_id($parent, $child_id);
  358. }
  359. }
  360. }
  361. }
  362. $entry_ids = $node->entry_ids;
  363. if ( ! empty($entry_ids))
  364. {
  365. $all_entry_ids[] = call_user_func_array('array_merge', $entry_ids);
  366. }
  367. }
  368. if ( ! count($all_entry_ids))
  369. {
  370. return array();
  371. }
  372. return call_user_func_array('array_merge', $all_entry_ids);
  373. }
  374. // --------------------------------------------------------------------
  375. /**
  376. * Parse Paths to Leaves
  377. *
  378. * Takes the leaf paths data returned by _get_leaves() and turns it into a form
  379. * that's more useable by PHP. It breaks each row down into arrays with keys
  380. * that we can then use to build a tree.
  381. *
  382. * @param mixed[] The array of leaves with field and entry_ids and the database returned keys.
  383. * @return mixed[] An array with the keys parsed.
  384. */
  385. protected function _parse_leaves(array $leaves)
  386. {
  387. $parsed_leaves = array();
  388. foreach ($leaves as $leaf)
  389. {
  390. $i = 0;
  391. while (isset($leaf['L'.$i.'_field']))
  392. {
  393. $field_id = $leaf['L'.$i.'_field'];
  394. $entry_id = (int) $leaf['L'.$i.'_id'];
  395. $parent_id = (int) $leaf['L'.$i.'_parent'];
  396. if ($entry_id == NULL)
  397. {
  398. break;
  399. }
  400. $field_name = $this->relationship_field_names[$field_id];
  401. if ( ! isset($parsed_leaves[$i]))
  402. {
  403. $parsed_leaves[$i] = array();
  404. }
  405. if ( ! isset($parsed_leaves[$i][$field_id]))
  406. {
  407. $parsed_leaves[$i][$field_id] = array();
  408. }
  409. if ( ! isset($parsed_leaves[$i][$field_id][$parent_id]))
  410. {
  411. $parsed_leaves[$i][$field_id][$parent_id] = array();
  412. }
  413. $parsed_leaves[$i++][$field_id][$parent_id][] = array(
  414. 'id' => $entry_id,
  415. 'field' => $field_name,
  416. 'parent' => $parent_id
  417. );
  418. }
  419. }
  420. return $parsed_leaves;
  421. }
  422. }
  423. /* End of file Tree_builder.php */
  424. /* Location: ./system/expressionengine/libraries/relationship_parser/Tree_builder.php */