123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720 |
- <?php
- namespace RocketTheme\Toolbox\Blueprints;
- /**
- * BlueprintSchema is used to define a data structure.
- *
- * @package RocketTheme\Toolbox\Blueprints
- * @author RocketTheme
- * @license MIT
- */
- class BlueprintSchema
- {
- /**
- * @var array
- */
- protected $items = [];
- /**
- * @var array
- */
- protected $rules = [];
- /**
- * @var array
- */
- protected $nested = [];
- /**
- * @var array
- */
- protected $dynamic = [];
- /**
- * @var array
- */
- protected $filter = ['validation' => true];
- /**
- * @var array
- */
- protected $ignoreFormKeys = ['fields' => 1];
- /**
- * @var array
- */
- protected $types = [];
- /**
- * Constructor.
- *
- * @param array $serialized Serialized content if available.
- */
- public function __construct($serialized = null)
- {
- if (is_array($serialized) && !empty($serialized)) {
- $this->items = (array) $serialized['items'];
- $this->rules = (array) $serialized['rules'];
- $this->nested = (array) $serialized['nested'];
- $this->dynamic = (array) $serialized['dynamic'];
- $this->filter = (array) $serialized['filter'];
- }
- }
- /**
- * @param array $types
- * @return $this
- */
- public function setTypes(array $types)
- {
- $this->types = $types;
- return $this;
- }
- /**
- * Restore Blueprints object.
- *
- * @param array $serialized
- * @return static
- */
- public static function restore(array $serialized)
- {
- return new static($serialized);
- }
- /**
- * Initialize blueprints with its dynamic fields.
- *
- * @return $this
- */
- public function init()
- {
- foreach ($this->dynamic as $key => $data) {
- $field = &$this->items[$key];
- foreach ($data as $property => $call) {
- $action = 'dynamic' . ucfirst($call['action']);
- if (method_exists($this, $action)) {
- $this->{$action}($field, $property, $call);
- }
- }
- }
- return $this;
- }
- /**
- * 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);
- }
- /**
- * Get value by using dot notation for nested arrays/objects.
- *
- * @example $value = $data->get('this.is.my.nested.variable');
- *
- * @param string $name Dot separated path to the requested value.
- * @param mixed $default Default value (or null).
- * @param string $separator Separator, defaults to '.'
- *
- * @return mixed Value.
- */
- public function get($name, $default = null, $separator = '.')
- {
- $name = $separator != '.' ? strtr($name, $separator, '.') : $name;
- return isset($this->items[$name]) ? $this->items[$name] : $default;
- }
- /**
- * Set value by using dot notation for nested arrays/objects.
- *
- * @example $value = $data->set('this.is.my.nested.variable', $newField);
- *
- * @param string $name Dot separated path to the requested value.
- * @param mixed $value New value.
- * @param string $separator Separator, defaults to '.'
- */
- public function set($name, $value, $separator = '.')
- {
- $name = $separator != '.' ? strtr($name, $separator, '.') : $name;
- $this->items[$name] = $value;
- $this->addProperty($name);
- }
- /**
- * Define value by using dot notation for nested arrays/objects.
- *
- * @example $value = $data->set('this.is.my.nested.variable', true);
- *
- * @param string $name Dot separated path to the requested value.
- * @param mixed $value New value.
- * @param string $separator Separator, defaults to '.'
- */
- public function def($name, $value, $separator = '.')
- {
- $this->set($name, $this->get($name, $value, $separator), $separator);
- }
- /**
- * @return array
- * @deprecated
- */
- public function toArray()
- {
- return $this->getState();
- }
- /**
- * Convert object into an array.
- *
- * @return array
- */
- public function getState()
- {
- return [
- 'items' => $this->items,
- 'rules' => $this->rules,
- 'nested' => $this->nested,
- 'dynamic' => $this->dynamic,
- 'filter' => $this->filter
- ];
- }
- /**
- * Get nested structure containing default values defined in the blueprints.
- *
- * Fields without default value are ignored in the list.
- *
- * @return array
- */
- public function getDefaults()
- {
- return $this->buildDefaults($this->nested);
- }
- /**
- * Embed an array to the blueprint.
- *
- * @param $name
- * @param array $value
- * @param string $separator
- * @param bool $merge Merge fields instead replacing them.
- * @return $this
- */
- public function embed($name, array $value, $separator = '.', $merge = false)
- {
- if (isset($value['rules'])) {
- $this->rules = array_merge($this->rules, $value['rules']);
- }
- $name = $separator != '.' ? strtr($name, $separator, '.') : $name;
- if (isset($value['form'])) {
- $form = array_diff_key($value['form'], ['fields' => 1, 'field' => 1]);
- } else {
- $form = [];
- }
- $items = isset($this->items[$name]) ? $this->items[$name] : ['type' => '_root', 'form_field' => false];
- $this->items[$name] = $items;
- $this->addProperty($name);
- $prefix = $name ? $name . '.' : '';
- $params = array_intersect_key($form, $this->filter);
- $location = [$name];
- if (isset($value['form']['field'])) {
- $this->parseFormField($name, $value['form']['field'], $params, $prefix, '', $merge, $location);
- } elseif (isset($value['form']['fields'])) {
- $this->parseFormFields($value['form']['fields'], $params, $prefix, '', $merge, $location);
- }
- $this->items[$name] += ['form' => $form];
- return $this;
- }
- /**
- * Merge two arrays by using blueprints.
- *
- * @param array $data1
- * @param array $data2
- * @param string $name Optional
- * @param string $separator Optional
- * @return array
- */
- public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
- {
- $nested = $this->getNested($name, $separator);
- if (!is_array($nested)) {
- $nested = [];
- }
- return $this->mergeArrays($data1, $data2, $nested);
- }
- /**
- * Get the property with given path.
- *
- * @param string $path
- * @param string $separator
- * @return mixed
- */
- public function getProperty($path = null, $separator = '.')
- {
- $name = $this->getPropertyName($path, $separator);
- $property = $this->get($name);
- $nested = $this->getNested($name);
- return $this->getPropertyRecursion($property, $nested);
- }
- /**
- * Returns name of the property with given path.
- *
- * @param string $path
- * @param string $separator
- * @return string
- */
- public function getPropertyName($path = null, $separator = '.')
- {
- $parts = explode($separator, $path);
- $nested = $this->nested;
- $result = [];
- while (($part = array_shift($parts)) !== null) {
- if (!isset($nested[$part])) {
- if (isset($nested['*'])) {
- $part = '*';
- } else {
- return implode($separator, array_merge($result, [$part], $parts));
- }
- }
- $result[] = $part;
- $nested = $nested[$part];
- }
- return implode('.', $result);
- }
- /**
- * Return data fields that do not exist in blueprints.
- *
- * @param array $data
- * @param string $prefix
- * @return array
- */
- public function extra(array $data, $prefix = '')
- {
- $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] : [];
- }
- }
- // Check if the form cannot have extra fields.
- if (isset($rules[''])) {
- $rule = $this->items[''];
- if (isset($rule['type']) && $rule['type'] != '_root') {
- return [];
- }
- }
- return $this->extraArray($data, $rules, $prefix);
- }
- /**
- * Get the property with given path.
- *
- * @param $property
- * @param $nested
- * @return mixed
- */
- protected function getPropertyRecursion($property, $nested)
- {
- if (!isset($property['type']) || empty($nested) || !is_array($nested)) {
- return $property;
- }
- if ($property['type'] === '_root') {
- foreach ($nested as $key => $value) {
- if ($key === '') {
- continue;
- }
- $name = is_array($value) ? $key : $value;
- $property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
- }
- } elseif ($property['type'] === '_parent' || !empty($property['array'])) {
- foreach ($nested as $key => $value) {
- $name = is_array($value) ? "{$property['name']}.{$key}" : $value;
- $property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
- }
- }
- return $property;
- }
- /**
- * Get property from the definition.
- *
- * @param string $path Comma separated path to the property.
- * @param string $separator
- * @return array|string|null
- * @internal
- */
- protected function getNested($path = null, $separator = '.')
- {
- if (!$path) {
- return $this->nested;
- }
- $parts = explode($separator, $path);
- $item = array_pop($parts);
- $nested = $this->nested;
- foreach ($parts as $part) {
- if (!isset($nested[$part])) {
- $part = '*';
- if (!isset($nested[$part])) {
- return [];
- }
- }
- $nested = $nested[$part];
- }
- return isset($nested[$item]) ? $nested[$item] : (isset($nested['*']) ? $nested['*'] : null);
- }
- /**
- * @param array $nested
- * @return array
- */
- protected function buildDefaults(array $nested)
- {
- $defaults = [];
- foreach ($nested as $key => $value) {
- if ($key === '*') {
- // TODO: Add support for adding defaults to collections.
- continue;
- }
- if (is_array($value)) {
- // Recursively fetch the items.
- $list = $this->buildDefaults($value);
- // Only return defaults if there are any.
- if (!empty($list)) {
- $defaults[$key] = $list;
- }
- } else {
- // We hit a field; get default from it if it exists.
- $item = $this->get($value);
- // Only return default value if it exists.
- if (isset($item['default'])) {
- $defaults[$key] = $item['default'];
- }
- }
- }
- return $defaults;
- }
- /**
- * @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->items[$val] : null;
- if (!empty($rule['type']) && $rule['type'][0] === '_'
- || (array_key_exists($key, $data1) && is_array($data1[$key]) && is_array($field) && is_array($val) && !isset($val['*']))
- ) {
- // Array has been defined in blueprints and is not a collection of items.
- $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
- } else {
- // Otherwise just take value from the data2.
- $data1[$key] = $field;
- }
- }
- return $data1;
- }
- /**
- * Gets all field definitions from the blueprints.
- *
- * @param array $fields Fields to parse.
- * @param array $params Property parameters.
- * @param string $prefix Property prefix.
- * @param string $parent Parent property.
- * @param bool $merge Merge fields instead replacing them.
- * @param array $formPath
- */
- protected function parseFormFields(array $fields, array $params, $prefix = '', $parent = '', $merge = false, array $formPath = [])
- {
- if (isset($fields['type']) && !is_array($fields['type'])) {
- return;
- }
- // Go though all the fields in current level.
- foreach ($fields as $key => $field) {
- $this->parseFormField($key, $field, $params, $prefix, $parent, $merge, $formPath);
- }
- }
- /**
- * @param string $key
- * @param array $field
- * @param array $params
- * @param string $prefix
- * @param string $parent
- * @param bool $merge
- * @param array $formPath
- */
- protected function parseFormField($key, array $field, array $params, $prefix = '', $parent = '', $merge = false, array $formPath = [])
- {
- // Skip illegal field (needs to be an array).
- if (!is_array($field)) {
- return;
- }
- $key = $this->getFieldKey($key, $prefix, $parent);
- $newPath = array_merge($formPath, [$key]);
- $properties = array_diff_key($field, $this->ignoreFormKeys) + $params;
- $properties['name'] = $key;
- // Set default properties for the field type.
- $type = isset($properties['type']) ? $properties['type'] : '';
- if (isset($this->types[$type])) {
- $properties += $this->types[$type];
- }
- // Merge properties with existing ones.
- if ($merge && isset($this->items[$key])) {
- $properties += $this->items[$key];
- }
- $isInputField = !isset($properties['input@']) || $properties['input@'];
- if (!$isInputField) {
- // Remove property if it exists.
- if (isset($this->items[$key])) {
- $this->removeProperty($key);
- }
- } elseif (!isset($this->items[$key])) {
- // Add missing property.
- $this->addProperty($key);
- }
- if (isset($field['fields'])) {
- // Recursively get all the nested fields.
- $isArray = !empty($properties['array']);
- $newParams = array_intersect_key($properties, $this->filter);
- $this->parseFormFields($field['fields'], $newParams, $prefix, $key . ($isArray ? '.*': ''), $merge, $newPath);
- } else {
- if (!isset($this->items[$key])) {
- // Add parent rules.
- $path = explode('.', $key);
- array_pop($path);
- $parent = '';
- foreach ($path as $part) {
- $parent .= ($parent ? '.' : '') . $part;
- if (!isset($this->items[$parent])) {
- $this->items[$parent] = ['type' => '_parent', 'name' => $parent, 'form_field' => false];
- }
- }
- }
- if ($isInputField) {
- $this->parseProperties($key, $properties);
- }
- }
- if ($isInputField) {
- $this->items[$key] = $properties;
- }
- }
- protected function getFieldKey($key, $prefix, $parent)
- {
- // Set name from the array key.
- if ($key && $key[0] == '.') {
- return ($parent ?: rtrim($prefix, '.')) . $key;
- }
- return $prefix . $key;
- }
- protected function parseProperties($key, array &$properties)
- {
- $key = ltrim($key, '.');
- if (!empty($properties['data'])) {
- $this->dynamic[$key] = $properties['data'];
- }
- foreach ($properties as $name => $value) {
- if (!empty($name) && ($name[0] === '@' || $name[strlen($name) - 1] === '@')) {
- $list = explode('-', trim($name, '@'), 2);
- $action = array_shift($list);
- $property = array_shift($list);
- $this->dynamic[$key][$property] = ['action' => $action, 'params' => $value];
- }
- }
- // Initialize predefined validation rule.
- if (isset($properties['validate']['rule'])) {
- $properties['validate'] += $this->getRule($properties['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]) || !is_array($nested[$part])) {
- $nested[$part] = [];
- }
- $nested = &$nested[$part];
- }
- if (!isset($nested[$item])) {
- $nested[$item] = $path;
- }
- }
- /**
- * Remove property to the definition.
- *
- * @param string $path Comma separated path to the property.
- * @internal
- */
- protected function removeProperty($path)
- {
- $parts = explode('.', $path);
- $item = array_pop($parts);
- $nested = &$this->nested;
- foreach ($parts as $part) {
- if (!isset($nested[$part]) || !is_array($nested[$part])) {
- return;
- }
- $nested = &$nested[$part];
- }
- if (isset($nested[$item])) {
- unset($nested[$item]);
- }
- }
- /**
- * @param $rule
- * @return array
- * @internal
- */
- protected function getRule($rule)
- {
- if (isset($this->rules[$rule]) && is_array($this->rules[$rule])) {
- return $this->rules[$rule];
- }
- return [];
- }
- /**
- * @param array $data
- * @param array $rules
- * @param string $prefix
- * @return array
- * @internal
- */
- protected function extraArray(array $data, array $rules, $prefix)
- {
- $array = [];
- foreach ($data as $key => $field) {
- $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
- $rule = is_string($val) ? $this->items[$val] : null;
- if ($rule || isset($val['*'])) {
- // 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;
- }
- /**
- * @param array $field
- * @param string $property
- * @param array $call
- */
- protected function dynamicData(array &$field, $property, array $call)
- {
- $params = $call['params'];
- if (is_array($params)) {
- $function = array_shift($params);
- } else {
- $function = $params;
- $params = [];
- }
- $list = preg_split('/::/', $function, 2);
- $f = array_pop($list);
- $o = array_pop($list);
- if (!$o) {
- if (function_exists($f)) {
- $data = call_user_func_array($f, $params);
- }
- } else {
- if (method_exists($o, $f)) {
- $data = call_user_func_array(array($o, $f), $params);
- }
- }
- // 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;
- }
- }
- }
- }
|