Blueprints.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. namespace RocketTheme\Toolbox\Blueprints;
  3. /**
  4. * Blueprints can be used to define a data structure.
  5. *
  6. * @package RocketTheme\Toolbox\Blueprints
  7. * @author RocketTheme
  8. * @license MIT
  9. */
  10. class Blueprints
  11. {
  12. /**
  13. * @var array
  14. */
  15. protected $items = [];
  16. /**
  17. * @var array
  18. */
  19. protected $rules = [];
  20. /**
  21. * @var array
  22. */
  23. protected $nested = [];
  24. /**
  25. * @var array
  26. */
  27. protected $filter = ['validation' => true];
  28. /**
  29. * Constructor.
  30. *
  31. * @param array $serialized Serialized content if available.
  32. */
  33. public function __construct(array $serialized = null)
  34. {
  35. if ($serialized) {
  36. $this->items = (array) $serialized['items'];
  37. $this->rules = (array) $serialized['rules'];
  38. $this->nested = (array) $serialized['nested'];
  39. $this->filter = (array) $serialized['filter'];
  40. }
  41. }
  42. /**
  43. * Set filter for inherited properties.
  44. *
  45. * @param array $filter List of field names to be inherited.
  46. */
  47. public function setFilter(array $filter)
  48. {
  49. $this->filter = array_flip($filter);
  50. }
  51. /**
  52. * Get value by using dot notation for nested arrays/objects.
  53. *
  54. * @example $value = $data->get('this.is.my.nested.variable');
  55. *
  56. * @param string $name Dot separated path to the requested value.
  57. * @param mixed $default Default value (or null).
  58. * @param string $separator Separator, defaults to '.'
  59. *
  60. * @return mixed Value.
  61. */
  62. public function get($name, $default = null, $separator = '.')
  63. {
  64. $name = $separator != '.' ? strtr($name, $separator, '.') : $name;
  65. return isset($this->items[$name]) ? $this->items[$name] : $default;
  66. }
  67. /**
  68. * Set value by using dot notation for nested arrays/objects.
  69. *
  70. * @example $value = $data->set('this.is.my.nested.variable', $newField);
  71. *
  72. * @param string $name Dot separated path to the requested value.
  73. * @param mixed $value New value.
  74. * @param string $separator Separator, defaults to '.'
  75. */
  76. public function set($name, $value, $separator = '.')
  77. {
  78. $name = $separator != '.' ? strtr($name, $separator, '.') : $name;
  79. $this->items[$name] = $value;
  80. $this->addProperty($name);
  81. }
  82. /**
  83. * Define value by using dot notation for nested arrays/objects.
  84. *
  85. * @example $value = $data->set('this.is.my.nested.variable', true);
  86. *
  87. * @param string $name Dot separated path to the requested value.
  88. * @param mixed $value New value.
  89. * @param string $separator Separator, defaults to '.'
  90. */
  91. public function def($name, $value, $separator = '.')
  92. {
  93. $this->set($name, $this->get($name, $value, $separator), $separator);
  94. }
  95. /**
  96. * Convert object into an array.
  97. *
  98. * @return array
  99. */
  100. public function toArray()
  101. {
  102. return ['items' => $this->items, 'rules' => $this->rules, 'nested' => $this->nested, 'filter' => $this->filter];
  103. }
  104. /**
  105. * Get nested structure containing default values defined in the blueprints.
  106. *
  107. * Fields without default value are ignored in the list.
  108. *
  109. * @return array
  110. */
  111. public function getDefaults()
  112. {
  113. return $this->buildDefaults($this->nested);
  114. }
  115. /**
  116. * Embed an array to the blueprint.
  117. *
  118. * @param $name
  119. * @param array $value
  120. * @param string $separator
  121. * @return $this
  122. */
  123. public function embed($name, array $value, $separator = '.')
  124. {
  125. if (isset($value['rules'])) {
  126. $this->rules = array_merge($this->rules, $value['rules']);
  127. }
  128. if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
  129. return $this;
  130. }
  131. $prefix = $name ? ($separator != '.' ? strtr($name, $separator, '.') : $name) . '.' : '';
  132. $params = array_intersect_key($this->filter, $value);
  133. $this->parseFormFields($value['form']['fields'], $params, $prefix);
  134. return $this;
  135. }
  136. /**
  137. * Merge two arrays by using blueprints.
  138. *
  139. * @param array $data1
  140. * @param array $data2
  141. * @param string $name Optional
  142. * @param string $separator Optional
  143. * @return array
  144. */
  145. public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
  146. {
  147. $nested = $this->getProperty($name, $separator);
  148. return $this->mergeArrays($data1, $data2, $nested);
  149. }
  150. /**
  151. * @param array $nested
  152. * @return array
  153. */
  154. protected function buildDefaults(array &$nested)
  155. {
  156. $defaults = [];
  157. foreach ($nested as $key => $value) {
  158. if ($key === '*') {
  159. // TODO: Add support for adding defaults to collections.
  160. continue;
  161. }
  162. if (is_array($value)) {
  163. // Recursively fetch the items.
  164. $list = $this->buildDefaults($value);
  165. // Only return defaults if there are any.
  166. if (!empty($list)) {
  167. $defaults[$key] = $list;
  168. }
  169. } else {
  170. // We hit a field; get default from it if it exists.
  171. $item = $this->get($value);
  172. // Only return default value if it exists.
  173. if (isset($item['default'])) {
  174. $defaults[$key] = $item['default'];
  175. }
  176. }
  177. }
  178. return $defaults;
  179. }
  180. /**
  181. * @param array $data1
  182. * @param array $data2
  183. * @param array $rules
  184. * @return array
  185. * @internal
  186. */
  187. protected function mergeArrays(array $data1, array $data2, array $rules)
  188. {
  189. foreach ($data2 as $key => $field) {
  190. $val = isset($rules[$key]) ? $rules[$key] : null;
  191. $rule = is_string($val) ? $this->items[$val] : null;
  192. if ($rule && $rule['type'] === '_parent' || (array_key_exists($key, $data1) && is_array($data1[$key]) && is_array($field) && is_array($val) && !isset($val['*']))) {
  193. // Array has been defined in blueprints and is not a collection of items.
  194. $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
  195. } else {
  196. // Otherwise just take value from the data2.
  197. $data1[$key] = $field;
  198. }
  199. }
  200. return $data1;
  201. }
  202. /**
  203. * Gets all field definitions from the blueprints.
  204. *
  205. * @param array $fields
  206. * @param array $params
  207. * @param string $prefix
  208. * @param string $parent
  209. * @internal
  210. */
  211. protected function parseFormFields(array &$fields, array $params, $prefix = '', $parent = '')
  212. {
  213. // Go though all the fields in current level.
  214. foreach ($fields as $key => &$field) {
  215. // Set name from the array key.
  216. if ($key && $key[0] == '.') {
  217. $key = ($parent ?: rtrim($prefix, '.')) . $key;
  218. } else {
  219. $key = $prefix . $key;
  220. }
  221. $field['name'] = $key;
  222. $field += $params;
  223. if (isset($field['fields'])) {
  224. $isArray = !empty($field['array']);
  225. // Recursively get all the nested fields.
  226. $newParams = array_intersect_key($this->filter, $field);
  227. $this->parseFormFields($field['fields'], $newParams, $prefix, $key . ($isArray ? '.*': ''));
  228. } else {
  229. // Add rule.
  230. $path = explode('.', $key);
  231. array_pop($path);
  232. $parent = '';
  233. foreach ($path as $part) {
  234. $parent .= ($parent ? '.' : '') . $part;
  235. if (!isset($this->items[$parent])) {
  236. $this->items[$parent] = ['type' => '_parent', 'name' => $parent];
  237. }
  238. }
  239. $this->items[$key] = &$field;
  240. $this->addProperty($key);
  241. foreach ($field as $name => $value) {
  242. if (substr($name, 0, 6) == '@data-') {
  243. $property = substr($name, 6);
  244. if (is_array($value)) {
  245. $func = array_shift($value);
  246. } else {
  247. $func = $value;
  248. $value = array();
  249. }
  250. list($o, $f) = preg_split('/::/', $func);
  251. if (!$f && function_exists($o)) {
  252. $data = call_user_func_array($o, $value);
  253. } elseif ($f && method_exists($o, $f)) {
  254. $data = call_user_func_array(array($o, $f), $value);
  255. }
  256. // If function returns a value,
  257. if (isset($data)) {
  258. if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
  259. // Combine field and @data-field together.
  260. $field[$property] += $data;
  261. } else {
  262. // Or create/replace field with @data-field.
  263. $field[$property] = $data;
  264. }
  265. }
  266. }
  267. }
  268. // Initialize predefined validation rule.
  269. if (isset($field['validate']['rule'])) {
  270. $field['validate'] += $this->getRule($field['validate']['rule']);
  271. }
  272. }
  273. }
  274. }
  275. /**
  276. * Get property from the definition.
  277. *
  278. * @param string $path Comma separated path to the property.
  279. * @param string $separator
  280. * @return array
  281. * @internal
  282. */
  283. public function getProperty($path = null, $separator = '.')
  284. {
  285. if (!$path) {
  286. return $this->nested;
  287. }
  288. $parts = explode($separator, $path);
  289. $item = array_pop($parts);
  290. $nested = $this->nested;
  291. foreach ($parts as $part) {
  292. if (!isset($nested[$part])) {
  293. return [];
  294. }
  295. $nested = $nested[$part];
  296. }
  297. return isset($nested[$item]) ? $nested[$item] : [];
  298. }
  299. /**
  300. * Add property to the definition.
  301. *
  302. * @param string $path Comma separated path to the property.
  303. * @internal
  304. */
  305. protected function addProperty($path)
  306. {
  307. $parts = explode('.', $path);
  308. $item = array_pop($parts);
  309. $nested = &$this->nested;
  310. foreach ($parts as $part) {
  311. if (!isset($nested[$part])) {
  312. $nested[$part] = array();
  313. }
  314. $nested = &$nested[$part];
  315. }
  316. if (!isset($nested[$item])) {
  317. $nested[$item] = $path;
  318. }
  319. }
  320. /**
  321. * @param $rule
  322. * @return array
  323. * @internal
  324. */
  325. protected function getRule($rule)
  326. {
  327. if (isset($this->rules[$rule]) && is_array($this->rules[$rule])) {
  328. return $this->rules[$rule];
  329. }
  330. return array();
  331. }
  332. }