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('Validation failed: %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']}"); } } } }