Blueprint.php 14 KB

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