Exporter.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\data\source\mongo_db;
  9. use lithium\util\Set;
  10. class Exporter extends \lithium\core\StaticObject {
  11. protected static $_classes = array(
  12. 'set' => 'lithium\data\collection\DocumentSet'
  13. );
  14. protected static $_commands = array(
  15. 'create' => null,
  16. 'update' => '$set',
  17. 'increment' => '$inc',
  18. 'remove' => '$unset',
  19. 'rename' => '$rename'
  20. );
  21. public static function get($type, $export, array $options = array()) {
  22. $defaults = array('whitelist' => array());
  23. $options += $defaults;
  24. if (!method_exists(get_called_class(), $method = "_{$type}") || !$export) {
  25. return;
  26. }
  27. return static::$method($export, array('finalize' => true) + $options);
  28. }
  29. public static function toCommand($changes) {
  30. $result = array();
  31. foreach (static::$_commands as $from => $to) {
  32. if (!isset($changes[$from])) {
  33. continue;
  34. }
  35. if (!$to) {
  36. $result = array_merge($result, $changes[$from]);
  37. }
  38. $result[$to] = $changes[$from];
  39. }
  40. unset($result['$set']['_id']);
  41. return $result;
  42. }
  43. protected static function _create($export, array $options) {
  44. $export += array('data' => array(), 'update' => array(), 'key' => '');
  45. $data = Set::merge($export['data'], $export['update']);
  46. if (array_keys($data) == range(0, count($data) - 1)) {
  47. $data = $export['update'];
  48. }
  49. $localOpts = array('finalize' => false) + $options;
  50. foreach ($data as $key => $val) {
  51. if (is_object($val) && method_exists($val, 'export')) {
  52. $data[$key] = static::_create($val->export($options), $localOpts);
  53. }
  54. }
  55. return ($options['finalize']) ? array('create' => $data) : $data;
  56. }
  57. /**
  58. * Calculates changesets for update operations, and produces an array which can be converted to
  59. * a set of native MongoDB update operations.
  60. *
  61. * @todo Implement remove and rename.
  62. * @param array $export An array of fields exported from a call to `Document::export()`, and
  63. * should contain the following keys:
  64. * - `'data'` _array_: An array representing the original data loaded from the
  65. * database for the document.
  66. * - `'update'` _array_: An array representing the current state of the document,
  67. * containing any modifications made.
  68. * - `'key'` _string_: If this is a nested document, this is a dot-separated path
  69. * from the root-level document.
  70. * @return array Returns an array representing the changes to be made to the document. These
  71. * are converted to database-native commands by the `toCommand()` method.
  72. */
  73. protected static function _update($export) {
  74. $export += array(
  75. 'data' => array(),
  76. 'update' => array(),
  77. 'remove' => array(),
  78. 'rename' => array(),
  79. 'key' => ''
  80. );
  81. $path = $export['key'] ? "{$export['key']}." : "";
  82. $result = array('update' => array(), 'remove' => array());
  83. $left = static::_diff($export['data'], $export['update']);
  84. $right = static::_diff($export['update'], $export['data']);
  85. $objects = array_filter($export['update'], function($value) {
  86. return (is_object($value) && method_exists($value, 'export'));
  87. });
  88. array_map(function($key) use (&$left) { unset($left[$key]); }, array_keys($right));
  89. foreach ($left as $key => $value) {
  90. $result = static::_append($result, "{$path}{$key}", $value, 'remove');
  91. }
  92. $update = $right + $objects;
  93. foreach ($update as $key => $value) {
  94. $original = $export['data'];
  95. $isArray = is_object($value) && get_class($value) === static::$_classes['set'];
  96. $options = array(
  97. 'indexed' => null,
  98. 'handlers' => array(
  99. 'MongoDate' => function($value) { return $value; },
  100. 'MongoId' => function($value) { return $value; }
  101. )
  102. );
  103. if ($isArray) {
  104. $newValue = $value->to('array', $options);
  105. $originalValue = null;
  106. if (isset($original[$key])) {
  107. $originalValue = $original[$key]->to('array', $options);
  108. }
  109. if ($newValue !== $originalValue) {
  110. $value = $value->to('array', $options);
  111. }
  112. }
  113. $result = static::_append($result, "{$path}{$key}", $value, 'update');
  114. }
  115. return array_filter($result);
  116. }
  117. /**
  118. * Handle diffing operations between `Document` object states. Implemented because all of PHP's
  119. * array comparison functions are broken when working with objects.
  120. *
  121. * @param array $left The left-hand comparison array.
  122. * @param array $right The right-hand comparison array.
  123. * @return array Returns an array of the differences of `$left` compared to `$right`.
  124. */
  125. protected static function _diff($left, $right) {
  126. $result = array();
  127. foreach ($left as $key => $value) {
  128. if (!array_key_exists($key, $right) || $left[$key] !== $right[$key]) {
  129. $result[$key] = $value;
  130. }
  131. }
  132. return $result;
  133. }
  134. /**
  135. * Handles appending nested objects to document changesets.
  136. *
  137. * @param array $changes The full set of changes to be made to the database.
  138. * @param string $key The key of the field to append, which may be a dot-separated path if the
  139. * value is or is contained within a nested object.
  140. * @param mixed $value The value to append to the changeset. Can be a scalar value, array, a
  141. * nested object, or part of a nested object.
  142. * @param string $change The type of change, as to whether update/remove or rename etc.
  143. * @return array Returns the value of `$changes`, with any new changed values appended.
  144. */
  145. protected static function _append($changes, $key, $value, $change) {
  146. $options = array('finalize' => false);
  147. if (!is_object($value) || !method_exists($value, 'export')) {
  148. $changes[$change][$key] = ($change === 'update') ? $value : true;
  149. return $changes;
  150. }
  151. if (!$value->exists()) {
  152. $changes[$change][$key] = static::_create($value->export(), $options);
  153. return $changes;
  154. }
  155. if ($change === 'update') {
  156. $export = compact('key') + $value->export();
  157. return Set::merge($changes, static::_update($export));
  158. }
  159. if ($children = static::_update($value->export())) {
  160. return Set::merge($changes, $children);
  161. }
  162. $changes[$change][$key] = true;
  163. return $changes;
  164. }
  165. }
  166. ?>