PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/system/src/Grav/Common/Data/Blueprint.php

https://gitlab.com/x33n/grav
PHP | 432 lines | 243 code | 47 blank | 142 comment | 53 complexity | 151184a01459aa2ddc46e4f1edbfa3bb MD5 | raw file
  1. <?php
  2. namespace Grav\Common\Data;
  3. use RocketTheme\Toolbox\ArrayTraits\Export;
  4. /**
  5. * Blueprint handles the inside logic of blueprints.
  6. *
  7. * @author RocketTheme
  8. * @license MIT
  9. */
  10. class Blueprint
  11. {
  12. use Export, DataMutatorTrait;
  13. public $name;
  14. public $initialized = false;
  15. protected $items;
  16. protected $context;
  17. protected $fields;
  18. protected $rules = array();
  19. protected $nested = array();
  20. protected $filter = ['validation' => 1];
  21. /**
  22. * @param string $name
  23. * @param array $data
  24. * @param Blueprints $context
  25. */
  26. public function __construct($name, array $data = array(), Blueprints $context = null)
  27. {
  28. $this->name = $name;
  29. $this->items = $data;
  30. $this->context = $context;
  31. }
  32. /**
  33. * Set filter for inherited properties.
  34. *
  35. * @param array $filter List of field names to be inherited.
  36. */
  37. public function setFilter(array $filter)
  38. {
  39. $this->filter = array_flip($filter);
  40. }
  41. /**
  42. * Return all form fields.
  43. *
  44. * @return array
  45. */
  46. public function fields()
  47. {
  48. if (!isset($this->fields)) {
  49. $this->fields = [];
  50. $this->embed('', $this->items);
  51. }
  52. return $this->fields;
  53. }
  54. /**
  55. * Validate data against blueprints.
  56. *
  57. * @param array $data
  58. * @throws \RuntimeException
  59. */
  60. public function validate(array $data)
  61. {
  62. // Initialize data
  63. $this->fields();
  64. try {
  65. $this->validateArray($data, $this->nested);
  66. } catch (\RuntimeException $e) {
  67. throw new \RuntimeException(sprintf('Page validation failed: %s', $e->getMessage()));
  68. }
  69. }
  70. /**
  71. * Merge two arrays by using blueprints.
  72. *
  73. * @param array $data1
  74. * @param array $data2
  75. * @return array
  76. */
  77. public function mergeData(array $data1, array $data2)
  78. {
  79. // Initialize data
  80. $this->fields();
  81. return $this->mergeArrays($data1, $data2, $this->nested);
  82. }
  83. /**
  84. * Filter data by using blueprints.
  85. *
  86. * @param array $data
  87. * @return array
  88. */
  89. public function filter(array $data)
  90. {
  91. // Initialize data
  92. $this->fields();
  93. return $this->filterArray($data, $this->nested);
  94. }
  95. /**
  96. * Return data fields that do not exist in blueprints.
  97. *
  98. * @param array $data
  99. * @param string $prefix
  100. * @return array
  101. */
  102. public function extra(array $data, $prefix = '')
  103. {
  104. // Initialize data
  105. $this->fields();
  106. return $this->extraArray($data, $this->nested, $prefix);
  107. }
  108. /**
  109. * Extend blueprint with another blueprint.
  110. *
  111. * @param Blueprint $extends
  112. * @param bool $append
  113. */
  114. public function extend(Blueprint $extends, $append = false)
  115. {
  116. $blueprints = $append ? $this->items : $extends->toArray();
  117. $appended = $append ? $extends->toArray() : $this->items;
  118. $bref_stack = array(&$blueprints);
  119. $head_stack = array($appended);
  120. do {
  121. end($bref_stack);
  122. $bref = &$bref_stack[key($bref_stack)];
  123. $head = array_pop($head_stack);
  124. unset($bref_stack[key($bref_stack)]);
  125. foreach (array_keys($head) as $key) {
  126. if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) {
  127. $bref_stack[] = &$bref[$key];
  128. $head_stack[] = $head[$key];
  129. } else {
  130. $bref = array_merge($bref, array($key => $head[$key]));
  131. }
  132. }
  133. } while (count($head_stack));
  134. $this->items = $blueprints;
  135. }
  136. /**
  137. * Convert object into an array.
  138. *
  139. * @return array
  140. */
  141. public function getState()
  142. {
  143. return ['name' => $this->name, 'items' => $this->items, 'rules' => $this->rules, 'nested' => $this->nested];
  144. }
  145. /**
  146. * Embed an array to the blueprint.
  147. *
  148. * @param $name
  149. * @param array $value
  150. * @param string $separator
  151. */
  152. public function embed($name, array $value, $separator = '.')
  153. {
  154. if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
  155. return;
  156. }
  157. // Initialize data
  158. $this->fields();
  159. $prefix = $name ? strtr($name, $separator, '.') . '.' : '';
  160. $params = array_intersect_key($this->filter, $value);
  161. $this->parseFormFields($value['form']['fields'], $params, $prefix, $this->fields);
  162. }
  163. /**
  164. * @param array $data
  165. * @param array $rules
  166. * @throws \RuntimeException
  167. * @internal
  168. */
  169. protected function validateArray(array $data, array $rules)
  170. {
  171. $this->checkRequired($data, $rules);
  172. foreach ($data as $key => $field) {
  173. $val = isset($rules[$key]) ? $rules[$key] : null;
  174. $rule = is_string($val) ? $this->rules[$val] : null;
  175. if ($rule) {
  176. // Item has been defined in blueprints.
  177. Validation::validate($field, $rule);
  178. } elseif (is_array($field) && is_array($val)) {
  179. // Array has been defined in blueprints.
  180. $this->validateArray($field, $val);
  181. } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
  182. // Undefined/extra item.
  183. throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
  184. }
  185. }
  186. }
  187. /**
  188. * @param array $data
  189. * @param array $rules
  190. * @return array
  191. * @internal
  192. */
  193. protected function filterArray(array $data, array $rules)
  194. {
  195. $results = array();
  196. foreach ($data as $key => $field) {
  197. $val = isset($rules[$key]) ? $rules[$key] : null;
  198. $rule = is_string($val) ? $this->rules[$val] : null;
  199. if ($rule) {
  200. // Item has been defined in blueprints.
  201. $field = Validation::filter($field, $rule);
  202. } elseif (is_array($field) && is_array($val)) {
  203. // Array has been defined in blueprints.
  204. $field = $this->filterArray($field, $val);
  205. } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
  206. $field = null;
  207. }
  208. if (isset($field) && (!is_array($field) || !empty($field))) {
  209. $results[$key] = $field;
  210. }
  211. }
  212. return $results;
  213. }
  214. /**
  215. * @param array $data1
  216. * @param array $data2
  217. * @param array $rules
  218. * @return array
  219. * @internal
  220. */
  221. protected function mergeArrays(array $data1, array $data2, array $rules)
  222. {
  223. foreach ($data2 as $key => $field) {
  224. $val = isset($rules[$key]) ? $rules[$key] : null;
  225. $rule = is_string($val) ? $this->rules[$val] : null;
  226. if (!$rule && array_key_exists($key, $data1) && is_array($field) && is_array($val)) {
  227. // Array has been defined in blueprints.
  228. $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
  229. } else {
  230. // Otherwise just take value from the data2.
  231. $data1[$key] = $field;
  232. }
  233. }
  234. return $data1;
  235. }
  236. /**
  237. * @param array $data
  238. * @param array $rules
  239. * @param string $prefix
  240. * @return array
  241. * @internal
  242. */
  243. protected function extraArray(array $data, array $rules, $prefix)
  244. {
  245. $array = array();
  246. foreach ($data as $key => $field) {
  247. $val = isset($rules[$key]) ? $rules[$key] : null;
  248. $rule = is_string($val) ? $this->rules[$val] : null;
  249. if ($rule) {
  250. // Item has been defined in blueprints.
  251. } elseif (is_array($field) && is_array($val)) {
  252. // Array has been defined in blueprints.
  253. $array += $this->ExtraArray($field, $val, $prefix);
  254. } else {
  255. // Undefined/extra item.
  256. $array[$prefix.$key] = $field;
  257. }
  258. }
  259. return $array;
  260. }
  261. /**
  262. * Gets all field definitions from the blueprints.
  263. *
  264. * @param array $fields
  265. * @param array $params
  266. * @param string $prefix
  267. * @param array $current
  268. * @internal
  269. */
  270. protected function parseFormFields(array &$fields, $params, $prefix, array &$current)
  271. {
  272. // Go though all the fields in current level.
  273. foreach ($fields as $key => &$field) {
  274. $current[$key] = &$field;
  275. // Set name from the array key.
  276. $field['name'] = $prefix . $key;
  277. $field += $params;
  278. if (isset($field['fields'])) {
  279. // Recursively get all the nested fields.
  280. $newParams = array_intersect_key($this->filter, $field);
  281. $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']);
  282. } else {
  283. // Add rule.
  284. $this->rules[$prefix . $key] = &$field;
  285. $this->addProperty($prefix . $key);
  286. foreach ($field as $name => $value) {
  287. // Support nested blueprints.
  288. if ($this->context && $name == '@import') {
  289. $values = (array) $value;
  290. if (!isset($field['fields'])) {
  291. $field['fields'] = array();
  292. }
  293. foreach ($values as $bname) {
  294. $b = $this->context->get($bname);
  295. $field['fields'] = array_merge($field['fields'], $b->fields());
  296. }
  297. }
  298. // Support for callable data values.
  299. elseif (substr($name, 0, 6) == '@data-') {
  300. $property = substr($name, 6);
  301. if (is_array($value)) {
  302. $func = array_shift($value);
  303. } else {
  304. $func = $value;
  305. $value = array();
  306. }
  307. list($o, $f) = preg_split('/::/', $func);
  308. if (!$f && function_exists($o)) {
  309. $data = call_user_func_array($o, $value);
  310. } elseif ($f && method_exists($o, $f)) {
  311. $data = call_user_func_array(array($o, $f), $value);
  312. }
  313. // If function returns a value,
  314. if (isset($data)) {
  315. if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
  316. // Combine field and @data-field together.
  317. $field[$property] += $data;
  318. } else {
  319. // Or create/replace field with @data-field.
  320. $field[$property] = $data;
  321. }
  322. }
  323. }
  324. }
  325. // Initialize predefined validation rule.
  326. if (isset($field['validate']['rule'])) {
  327. $field['validate'] += $this->getRule($field['validate']['rule']);
  328. }
  329. }
  330. }
  331. }
  332. /**
  333. * Add property to the definition.
  334. *
  335. * @param string $path Comma separated path to the property.
  336. * @internal
  337. */
  338. protected function addProperty($path)
  339. {
  340. $parts = explode('.', $path);
  341. $item = array_pop($parts);
  342. $nested = &$this->nested;
  343. foreach ($parts as $part) {
  344. if (!isset($nested[$part])) {
  345. $nested[$part] = array();
  346. }
  347. $nested = &$nested[$part];
  348. }
  349. if (!isset($nested[$item])) {
  350. $nested[$item] = $path;
  351. }
  352. }
  353. /**
  354. * @param $rule
  355. * @return array
  356. * @internal
  357. */
  358. protected function getRule($rule)
  359. {
  360. if (isset($this->items['rules'][$rule]) && is_array($this->items['rules'][$rule])) {
  361. return $this->items['rules'][$rule];
  362. }
  363. return array();
  364. }
  365. /**
  366. * @param array $data
  367. * @param array $fields
  368. * @throws \RuntimeException
  369. * @internal
  370. */
  371. protected function checkRequired(array $data, array $fields)
  372. {
  373. foreach ($fields as $name => $field) {
  374. if (!is_string($field)) {
  375. continue;
  376. }
  377. $field = $this->rules[$field];
  378. if (isset($field['validate']['required'])
  379. && $field['validate']['required'] === true
  380. && empty($data[$name])) {
  381. throw new \RuntimeException("Missing required field: {$field['name']}");
  382. }
  383. }
  384. }
  385. }