123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- <?php
- namespace Grav\Common\Data;
- use Grav\Common\GravTrait;
- use RocketTheme\Toolbox\ArrayTraits\Export;
- /**
- * Blueprint handles the inside logic of blueprints.
- *
- * @author RocketTheme
- * @license MIT
- */
- class Blueprint
- {
- use Export, DataMutatorTrait, GravTrait;
- public $name;
- public $initialized = false;
- protected $items;
- protected $context;
- protected $fields;
- protected $rules = array();
- protected $nested = array();
- protected $filter = ['validation' => 1];
- /**
- * @param string $name
- * @param array $data
- * @param Blueprints $context
- */
- public function __construct($name, array $data = array(), Blueprints $context = null)
- {
- $this->name = $name;
- $this->items = $data;
- $this->context = $context;
- }
- /**
- * Set filter for inherited properties.
- *
- * @param array $filter List of field names to be inherited.
- */
- public function setFilter(array $filter)
- {
- $this->filter = array_flip($filter);
- }
- /**
- * Return all form fields.
- *
- * @return array
- */
- public function fields()
- {
- if (!isset($this->fields)) {
- $this->fields = [];
- $this->embed('', $this->items);
- }
- return $this->fields;
- }
- /**
- * Validate data against blueprints.
- *
- * @param array $data
- * @throws \RuntimeException
- */
- public function validate(array $data)
- {
- // Initialize data
- $this->fields();
- try {
- $this->validateArray($data, $this->nested);
- } catch (\RuntimeException $e) {
- throw new \RuntimeException(sprintf('<b>Validation failed:</b> %s', $e->getMessage()));
- }
- }
- /**
- * Merge two arrays by using blueprints.
- *
- * @param array $data1
- * @param array $data2
- * @return array
- */
- public function mergeData(array $data1, array $data2)
- {
- // Initialize data
- $this->fields();
- return $this->mergeArrays($data1, $data2, $this->nested);
- }
- /**
- * Filter data by using blueprints.
- *
- * @param array $data
- * @return array
- */
- public function filter(array $data)
- {
- // Initialize data
- $this->fields();
- return $this->filterArray($data, $this->nested);
- }
- /**
- * Return data fields that do not exist in blueprints.
- *
- * @param array $data
- * @param string $prefix
- * @return array
- */
- public function extra(array $data, $prefix = '')
- {
- // Initialize data
- $this->fields();
- $rules = $this->nested;
- // Drill down to prefix level
- if (!empty($prefix)) {
- $parts = explode('.', trim($prefix, '.'));
- foreach ($parts as $part) {
- $rules = isset($rules[$part]) ? $rules[$part] : [];
- }
- }
- return $this->extraArray($data, $rules, $prefix);
- }
- /**
- * Extend blueprint with another blueprint.
- *
- * @param Blueprint $extends
- * @param bool $append
- */
- public function extend(Blueprint $extends, $append = false)
- {
- $blueprints = $append ? $this->items : $extends->toArray();
- $appended = $append ? $extends->toArray() : $this->items;
- $bref_stack = array(&$blueprints);
- $head_stack = array($appended);
- do {
- end($bref_stack);
- $bref = &$bref_stack[key($bref_stack)];
- $head = array_pop($head_stack);
- unset($bref_stack[key($bref_stack)]);
- foreach (array_keys($head) as $key) {
- if (isset($key, $bref[$key]) && is_array($bref[$key]) && is_array($head[$key])) {
- $bref_stack[] = &$bref[$key];
- $head_stack[] = $head[$key];
- } else {
- $bref = array_merge($bref, array($key => $head[$key]));
- }
- }
- } while (count($head_stack));
- $this->items = $blueprints;
- }
- /**
- * Convert object into an array.
- *
- * @return array
- */
- public function getState()
- {
- return ['name' => $this->name, 'items' => $this->items, 'rules' => $this->rules, 'nested' => $this->nested];
- }
- /**
- * Embed an array to the blueprint.
- *
- * @param $name
- * @param array $value
- * @param string $separator
- */
- public function embed($name, array $value, $separator = '.')
- {
- if (!isset($value['form']['fields']) || !is_array($value['form']['fields'])) {
- return;
- }
- // Initialize data
- $this->fields();
- $prefix = $name ? strtr($name, $separator, '.') . '.' : '';
- $params = array_intersect_key($this->filter, $value);
- $this->parseFormFields($value['form']['fields'], $params, $prefix, $this->fields);
- }
- /**
- * @param array $data
- * @param array $rules
- * @throws \RuntimeException
- * @internal
- */
- protected function validateArray(array $data, array $rules)
- {
- $this->checkRequired($data, $rules);
- foreach ($data as $key => $field) {
- $val = isset($rules[$key]) ? $rules[$key] : null;
- $rule = is_string($val) ? $this->rules[$val] : null;
- if ($rule) {
- // Item has been defined in blueprints.
- Validation::validate($field, $rule);
- } elseif (is_array($field) && is_array($val)) {
- // Array has been defined in blueprints.
- $this->validateArray($field, $val);
- } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
- // Undefined/extra item.
- throw new \RuntimeException(sprintf('%s is not defined in blueprints', $key));
- }
- }
- }
- /**
- * @param array $data
- * @param array $rules
- * @return array
- * @internal
- */
- protected function filterArray(array $data, array $rules)
- {
- $results = array();
- foreach ($data as $key => $field) {
- $val = isset($rules[$key]) ? $rules[$key] : null;
- $rule = is_string($val) ? $this->rules[$val] : null;
- if ($rule) {
- // Item has been defined in blueprints.
- if (is_array($field) && count($field) == 1 && reset($field) == '') {
- continue;
- }
- $field = Validation::filter($field, $rule);
- } elseif (is_array($field) && is_array($val)) {
- // Array has been defined in blueprints.
- $field = $this->filterArray($field, $val);
- } elseif (isset($this->items['form']['validation']) && $this->items['form']['validation'] == 'strict') {
- $field = null;
- }
- if (isset($field) && (!is_array($field) || !empty($field))) {
- $results[$key] = $field;
- }
- }
- return $results;
- }
- /**
- * @param array $data1
- * @param array $data2
- * @param array $rules
- * @return array
- * @internal
- */
- protected function mergeArrays(array $data1, array $data2, array $rules)
- {
- foreach ($data2 as $key => $field) {
- $val = isset($rules[$key]) ? $rules[$key] : null;
- $rule = is_string($val) ? $this->rules[$val] : null;
- if (!$rule && array_key_exists($key, $data1) && is_array($field) && is_array($val)) {
- // Array has been defined in blueprints.
- $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
- } else {
- // Otherwise just take value from the data2.
- $data1[$key] = $field;
- }
- }
- return $data1;
- }
- /**
- * @param array $data
- * @param array $rules
- * @param string $prefix
- * @return array
- * @internal
- */
- protected function extraArray(array $data, array $rules, $prefix)
- {
- $array = array();
- foreach ($data as $key => $field) {
- $val = isset($rules[$key]) ? $rules[$key] : null;
- $rule = is_string($val) ? $this->rules[$val] : null;
- if ($rule) {
- // Item has been defined in blueprints.
- } elseif (is_array($field) && is_array($val)) {
- // Array has been defined in blueprints.
- $array += $this->ExtraArray($field, $val, $prefix . $key . '.');
- } else {
- // Undefined/extra item.
- $array[$prefix.$key] = $field;
- }
- }
- return $array;
- }
- /**
- * Gets all field definitions from the blueprints.
- *
- * @param array $fields
- * @param array $params
- * @param string $prefix
- * @param array $current
- * @internal
- */
- protected function parseFormFields(array &$fields, $params, $prefix, array &$current)
- {
- // Go though all the fields in current level.
- foreach ($fields as $key => &$field) {
- $current[$key] = &$field;
- // Set name from the array key.
- $field['name'] = $prefix . $key;
- $field += $params;
- if (isset($field['fields']) && (!isset($field['type']) || $field['type'] !== 'list')) {
- // Recursively get all the nested fields.
- $newParams = array_intersect_key($this->filter, $field);
- $this->parseFormFields($field['fields'], $newParams, $prefix, $current[$key]['fields']);
- } else if ($field['type'] !== 'ignore') {
- // Add rule.
- $this->rules[$prefix . $key] = &$field;
- $this->addProperty($prefix . $key);
- foreach ($field as $name => $value) {
- // Support nested blueprints.
- if ($this->context && $name == '@import') {
- $values = (array) $value;
- if (!isset($field['fields'])) {
- $field['fields'] = array();
- }
- foreach ($values as $bname) {
- $b = $this->context->get($bname);
- $field['fields'] = array_merge($field['fields'], $b->fields());
- }
- }
- // Support for callable data values.
- elseif (substr($name, 0, 6) == '@data-') {
- $property = substr($name, 6);
- if (is_array($value)) {
- $func = array_shift($value);
- } else {
- $func = $value;
- $value = array();
- }
- list($o, $f) = preg_split('/::/', $func);
- if (!$f && function_exists($o)) {
- $data = call_user_func_array($o, $value);
- } elseif ($f && method_exists($o, $f)) {
- $data = call_user_func_array(array($o, $f), $value);
- }
- // If function returns a value,
- if (isset($data)) {
- if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
- // Combine field and @data-field together.
- $field[$property] += $data;
- } else {
- // Or create/replace field with @data-field.
- $field[$property] = $data;
- }
- }
- }
- elseif (substr($name, 0, 8) == '@config-') {
- $property = substr($name, 8);
- $default = isset($field[$property]) ? $field[$property] : null;
- $config = self::getGrav()['config']->get($value, $default);
- if (!is_null($config)) {
- $field[$property] = $config;
- }
- }
- }
- // Initialize predefined validation rule.
- if (isset($field['validate']['rule']) && $field['type'] !== 'ignore') {
- $field['validate'] += $this->getRule($field['validate']['rule']);
- }
- }
- }
- }
- /**
- * Add property to the definition.
- *
- * @param string $path Comma separated path to the property.
- * @internal
- */
- protected function addProperty($path)
- {
- $parts = explode('.', $path);
- $item = array_pop($parts);
- $nested = &$this->nested;
- foreach ($parts as $part) {
- if (!isset($nested[$part])) {
- $nested[$part] = array();
- }
- $nested = &$nested[$part];
- }
- if (!isset($nested[$item])) {
- $nested[$item] = $path;
- }
- }
- /**
- * @param $rule
- * @return array
- * @internal
- */
- protected function getRule($rule)
- {
- if (isset($this->items['rules'][$rule]) && is_array($this->items['rules'][$rule])) {
- return $this->items['rules'][$rule];
- }
- return array();
- }
- /**
- * @param array $data
- * @param array $fields
- * @throws \RuntimeException
- * @internal
- */
- protected function checkRequired(array $data, array $fields)
- {
- foreach ($fields as $name => $field) {
- if (!is_string($field)) {
- continue;
- }
- $field = $this->rules[$field];
- if (isset($field['validate']['required'])
- && $field['validate']['required'] === true
- && empty($data[$name])) {
- throw new \RuntimeException("Missing required field: {$field['name']}");
- }
- }
- }
- }
|