"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/rockettheme/toolbox/Blueprints/src/BlueprintSchema.php" (1 Sep 2020, 20695 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "BlueprintSchema.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 namespace RocketTheme\Toolbox\Blueprints;
    3 
    4 /**
    5  * BlueprintSchema is used to define a data structure.
    6  *
    7  * @package RocketTheme\Toolbox\Blueprints
    8  * @author RocketTheme
    9  * @license MIT
   10  */
   11 class BlueprintSchema
   12 {
   13     /** @var array */
   14     protected $items = [];
   15 
   16     /** @var array */
   17     protected $rules = [];
   18 
   19     /** @var array */
   20     protected $nested = [];
   21 
   22     /** @var array */
   23     protected $dynamic = [];
   24 
   25     /** @var array */
   26     protected $filter = ['validation' => true];
   27 
   28     /** @var array */
   29     protected $ignoreFormKeys = ['fields' => 1];
   30 
   31     /** @var array */
   32     protected $types = [];
   33 
   34     /**
   35      * Constructor.
   36      *
   37      * @param array $serialized  Serialized content if available.
   38      */
   39     public function __construct($serialized = null)
   40     {
   41         if (\is_array($serialized) && !empty($serialized)) {
   42             $this->items = (array) $serialized['items'];
   43             $this->rules = (array) $serialized['rules'];
   44             $this->nested = (array) $serialized['nested'];
   45             $this->dynamic = (array) $serialized['dynamic'];
   46             $this->filter = (array) $serialized['filter'];
   47         }
   48     }
   49 
   50     /**
   51      * @param array $types
   52      * @return $this
   53      */
   54     public function setTypes(array $types)
   55     {
   56         $this->types = $types;
   57 
   58         return $this;
   59     }
   60 
   61     /**
   62      * Restore Blueprints object.
   63      *
   64      * @param array $serialized
   65      * @return static
   66      */
   67     public static function restore(array $serialized)
   68     {
   69         return new static($serialized);
   70     }
   71 
   72     /**
   73      * Initialize blueprints with its dynamic fields.
   74      *
   75      * @return $this
   76      */
   77     public function init()
   78     {
   79         foreach ($this->dynamic as $key => $data) {
   80             $field = &$this->items[$key];
   81 
   82             foreach ($data as $property => $call) {
   83                 $action = 'dynamic' . ucfirst($call['action']);
   84 
   85                 if (method_exists($this, $action)) {
   86                     $this->{$action}($field, $property, $call);
   87                 }
   88             }
   89         }
   90 
   91         return $this;
   92     }
   93 
   94     /**
   95      * Set filter for inherited properties.
   96      *
   97      * @param array $filter     List of field names to be inherited.
   98      */
   99     public function setFilter(array $filter)
  100     {
  101         $this->filter = array_flip($filter);
  102     }
  103 
  104     /**
  105      * Get value by using dot notation for nested arrays/objects.
  106      *
  107      * @example $value = $data->get('this.is.my.nested.variable');
  108      *
  109      * @param string  $name       Dot separated path to the requested value.
  110      * @param mixed   $default    Default value (or null).
  111      * @param string  $separator  Separator, defaults to '.'
  112      *
  113      * @return mixed  Value.
  114      */
  115     public function get($name, $default = null, $separator = '.')
  116     {
  117         $name = $separator !== '.' ? str_replace($separator, '.', $name) : $name;
  118 
  119         return isset($this->items[$name]) ? $this->items[$name] : $default;
  120     }
  121 
  122     /**
  123      * Set value by using dot notation for nested arrays/objects.
  124      *
  125      * @example $value = $data->set('this.is.my.nested.variable', $newField);
  126      *
  127      * @param string  $name       Dot separated path to the requested value.
  128      * @param mixed   $value      New value.
  129      * @param string  $separator  Separator, defaults to '.'
  130      */
  131     public function set($name, $value, $separator = '.')
  132     {
  133         $name = $separator !== '.' ? str_replace($separator, '.', $name) : $name;
  134 
  135         $this->items[$name] = $value;
  136         $this->addProperty($name);
  137     }
  138 
  139     /**
  140      * Define value by using dot notation for nested arrays/objects.
  141      *
  142      * @example $value = $data->set('this.is.my.nested.variable', true);
  143      *
  144      * @param string  $name       Dot separated path to the requested value.
  145      * @param mixed   $value      New value.
  146      * @param string  $separator  Separator, defaults to '.'
  147      */
  148     public function def($name, $value, $separator = '.')
  149     {
  150         $this->set($name, $this->get($name, $value, $separator), $separator);
  151     }
  152 
  153     /**
  154      * @return array
  155      * @deprecated
  156      */
  157     public function toArray()
  158     {
  159         return $this->getState();
  160     }
  161 
  162     /**
  163      * Convert object into an array.
  164      *
  165      * @return array
  166      */
  167     public function getState()
  168     {
  169         return [
  170             'items' => $this->items,
  171             'rules' => $this->rules,
  172             'nested' => $this->nested,
  173             'dynamic' => $this->dynamic,
  174             'filter' => $this->filter
  175         ];
  176     }
  177 
  178     /**
  179      * Get nested structure containing default values defined in the blueprints.
  180      *
  181      * Fields without default value are ignored in the list.
  182      *
  183      * @return array
  184      */
  185     public function getDefaults()
  186     {
  187         return $this->buildDefaults($this->nested);
  188     }
  189 
  190     /**
  191      * Embed an array to the blueprint.
  192      *
  193      * @param $name
  194      * @param array $value
  195      * @param string $separator
  196      * @param bool $merge   Merge fields instead replacing them.
  197      * @return $this
  198      */
  199     public function embed($name, array $value, $separator = '.', $merge = false)
  200     {
  201         if (isset($value['rules'])) {
  202             $this->rules = array_merge($this->rules, $value['rules']);
  203         }
  204 
  205         $name = $separator !== '.' ? str_replace($separator, '.', $name) : $name;
  206 
  207         if (isset($value['form'])) {
  208             $form = array_diff_key($value['form'], ['fields' => 1, 'field' => 1]);
  209         } else {
  210             $form = [];
  211         }
  212 
  213         $items = isset($this->items[$name]) ? $this->items[$name] : ['type' => '_root', 'form_field' => false];
  214 
  215         $this->items[$name] = $items;
  216         $this->addProperty($name);
  217 
  218         $prefix = $name ? $name . '.' : '';
  219         $params = array_intersect_key($form, $this->filter);
  220         $location = [$name];
  221 
  222         if (isset($value['form']['field'])) {
  223             $this->parseFormField($name, $value['form']['field'], $params, $prefix, '', $merge, $location);
  224         } elseif (isset($value['form']['fields'])) {
  225             $this->parseFormFields($value['form']['fields'], $params, $prefix, '', $merge, $location);
  226         }
  227 
  228         $this->items[$name] += ['form' => $form];
  229 
  230         return $this;
  231     }
  232 
  233     /**
  234      * Merge two arrays by using blueprints.
  235      *
  236      * @param  array $data1
  237      * @param  array $data2
  238      * @param  string $name         Optional
  239      * @param  string $separator    Optional
  240      * @return array
  241      */
  242     public function mergeData(array $data1, array $data2, $name = null, $separator = '.')
  243     {
  244         $nested = $this->getNested($name, $separator);
  245 
  246         if (!\is_array($nested)) {
  247             $nested = [];
  248         }
  249 
  250         return $this->mergeArrays($data1, $data2, $nested);
  251     }
  252 
  253     /**
  254      * Get the property with given path.
  255      *
  256      * @param string $path
  257      * @param string $separator
  258      * @return mixed
  259      */
  260     public function getProperty($path = null, $separator = '.')
  261     {
  262         $name = $this->getPropertyName($path, $separator);
  263         $property = $this->get($name);
  264         $nested = $this->getNested($name);
  265 
  266         return $this->getPropertyRecursion($property, $nested);
  267     }
  268 
  269     /**
  270      * Returns name of the property with given path.
  271      *
  272      * @param string $path
  273      * @param string $separator
  274      * @return string
  275      */
  276     public function getPropertyName($path = null, $separator = '.')
  277     {
  278         $parts = explode($separator, $path);
  279         $nested = $this->nested;
  280 
  281         $result = [];
  282         while (($part = array_shift($parts)) !== null) {
  283             if (!isset($nested[$part])) {
  284                 if (isset($nested['*'])) {
  285                     $part = '*';
  286                 } else {
  287                     return implode($separator, array_merge($result, [$part], $parts));
  288                 }
  289             }
  290             $result[] = $part;
  291             $nested = $nested[$part];
  292         }
  293 
  294         return implode('.', $result);
  295     }
  296 
  297     /**
  298      * Return data fields that do not exist in blueprints.
  299      *
  300      * @param  array  $data
  301      * @param  string $prefix
  302      * @return array
  303      */
  304     public function extra(array $data, $prefix = '')
  305     {
  306         $rules = $this->nested;
  307 
  308         // Drill down to prefix level
  309         if (!empty($prefix)) {
  310             $parts = explode('.', trim($prefix, '.'));
  311             foreach ($parts as $part) {
  312                 $rules = isset($rules[$part]) ? $rules[$part] : [];
  313             }
  314         }
  315 
  316         // Check if the form cannot have extra fields.
  317         if (isset($rules[''])) {
  318             $rule = $this->items[''];
  319             if (isset($rule['type']) && $rule['type'] !== '_root') {
  320                 return [];
  321             }
  322         }
  323 
  324         return $this->extraArray($data, $rules, $prefix);
  325     }
  326 
  327     /**
  328      * Get the property with given path.
  329      *
  330      * @param $property
  331      * @param $nested
  332      * @return mixed
  333      */
  334     protected function getPropertyRecursion($property, $nested)
  335     {
  336         if (empty($nested) || !\is_array($nested) || !isset($property['type'])) {
  337             return $property;
  338         }
  339 
  340         if ($property['type'] === '_root') {
  341             foreach ($nested as $key => $value) {
  342                 if ($key === '') {
  343                     continue;
  344                 }
  345 
  346                 $name = \is_array($value) ? $key : $value;
  347                 $property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
  348             }
  349         } elseif ($property['type'] === '_parent' || !empty($property['array'])) {
  350             foreach ($nested as $key => $value) {
  351                 $name = \is_array($value) ? "{$property['name']}.{$key}" : $value;
  352                 $property['fields'][$key] = $this->getPropertyRecursion($this->get($name), $value);
  353             }
  354         }
  355 
  356         return $property;
  357     }
  358 
  359     /**
  360      * Get property from the definition.
  361      *
  362      * @param  string  $path  Comma separated path to the property.
  363      * @param  string  $separator
  364      * @return array|string|null
  365      * @internal
  366      */
  367     protected function getNested($path = null, $separator = '.')
  368     {
  369         if (!$path) {
  370             return $this->nested;
  371         }
  372         $parts = explode($separator, $path);
  373         $item = array_pop($parts);
  374 
  375         $nested = $this->nested;
  376         foreach ($parts as $part) {
  377             if (!isset($nested[$part])) {
  378                 $part = '*';
  379                 if (!isset($nested[$part])) {
  380                     return [];
  381                 }
  382             }
  383             $nested = $nested[$part];
  384         }
  385 
  386         return isset($nested[$item]) ? $nested[$item] : (isset($nested['*']) ? $nested['*'] : null);
  387     }
  388 
  389     /**
  390      * @param array $nested
  391      * @return array
  392      */
  393     protected function buildDefaults(array $nested)
  394     {
  395         $defaults = [];
  396 
  397         foreach ($nested as $key => $value) {
  398             if ($key === '*') {
  399                 // TODO: Add support for adding defaults to collections.
  400                 continue;
  401             }
  402 
  403             if (\is_array($value)) {
  404                 // Recursively fetch the items.
  405                 $list = $this->buildDefaults($value);
  406 
  407                 // Only return defaults if there are any.
  408                 if (!empty($list)) {
  409                     $defaults[$key] = $list;
  410                 }
  411             } else {
  412                 // We hit a field; get default from it if it exists.
  413                 $item = $this->get($value);
  414 
  415                 // Only return default value if it exists.
  416                 if (isset($item['default'])) {
  417                     $defaults[$key] = $item['default'];
  418                 }
  419             }
  420         }
  421 
  422         return $defaults;
  423     }
  424 
  425     /**
  426      * @param array $data1
  427      * @param array $data2
  428      * @param array $rules
  429      * @return array
  430      * @internal
  431      */
  432     protected function mergeArrays(array $data1, array $data2, array $rules)
  433     {
  434         foreach ($data2 as $key => $field) {
  435             $val = isset($rules[$key]) ? $rules[$key] : null;
  436             $rule = \is_string($val) ? $this->items[$val] : null;
  437 
  438             if ((array_key_exists($key, $data1) && \is_array($data1[$key]) && \is_array($field) && \is_array($val) && !isset($val['*']))
  439                 || (!empty($rule['type']) && strpos($rule['type'], '_') === 0)) {
  440                 // Array has been defined in blueprints and is not a collection of items.
  441                 $data1[$key] = $this->mergeArrays($data1[$key], $field, $val);
  442             } else {
  443                 // Otherwise just take value from the data2.
  444                 $data1[$key] = $field;
  445             }
  446         }
  447 
  448         return $data1;
  449     }
  450 
  451     /**
  452      * Gets all field definitions from the blueprints.
  453      *
  454      * @param array  $fields    Fields to parse.
  455      * @param array  $params    Property parameters.
  456      * @param string $prefix    Property prefix.
  457      * @param string $parent    Parent property.
  458      * @param bool   $merge     Merge fields instead replacing them.
  459      * @param array $formPath
  460      */
  461     protected function parseFormFields(array $fields, array $params, $prefix = '', $parent = '', $merge = false, array $formPath = [])
  462     {
  463         if (isset($fields['type']) && !\is_array($fields['type'])) {
  464             return;
  465         }
  466 
  467         // Go though all the fields in current level.
  468         foreach ($fields as $key => $field) {
  469             $this->parseFormField($key, $field, $params, $prefix, $parent, $merge, $formPath);
  470         }
  471     }
  472 
  473     /**
  474      * @param string $key
  475      * @param array $field
  476      * @param array $params
  477      * @param string $prefix
  478      * @param string $parent
  479      * @param bool $merge
  480      * @param array $formPath
  481      */
  482     protected function parseFormField($key, array $field, array $params, $prefix = '', $parent = '', $merge = false, array $formPath = [])
  483     {
  484         // Skip illegal field (needs to be an array).
  485         if (!\is_array($field)) {
  486             return;
  487         }
  488 
  489         $key = $this->getFieldKey($key, $prefix, $parent);
  490 
  491         $newPath = array_merge($formPath, [$key]);
  492 
  493         $properties = array_diff_key($field, $this->ignoreFormKeys) + $params;
  494         $properties['name'] = $key;
  495 
  496         // Set default properties for the field type.
  497         $type = isset($properties['type']) ? $properties['type'] : '';
  498         if (isset($this->types[$type])) {
  499             $properties += $this->types[$type];
  500         }
  501 
  502         // Merge properties with existing ones.
  503         if ($merge && isset($this->items[$key])) {
  504             $properties += $this->items[$key];
  505         }
  506 
  507         $isInputField = !isset($properties['input@']) || $properties['input@'];
  508 
  509         $propertyExists = isset($this->items[$key]);
  510         if (!$isInputField) {
  511             // Remove property if it exists.
  512             if ($propertyExists) {
  513                 $this->removeProperty($key);
  514             }
  515         } elseif (!$propertyExists) {
  516             // Add missing property.
  517             $this->addProperty($key);
  518         }
  519 
  520         if (isset($field['fields'])) {
  521             // Recursively get all the nested fields.
  522             $isArray = !empty($properties['array']);
  523             $newParams = array_intersect_key($properties, $this->filter);
  524             $this->parseFormFields($field['fields'], $newParams, $prefix, $key . ($isArray ? '.*': ''), $merge, $newPath);
  525         } else {
  526             if (!isset($this->items[$key])) {
  527                 // Add parent rules.
  528                 $path = explode('.', $key);
  529                 array_pop($path);
  530                 $parent = '';
  531 
  532                 foreach ($path as $part) {
  533                     $parent .= ($parent ? '.' : '') . $part;
  534                     if (!isset($this->items[$parent])) {
  535                         $this->items[$parent] = ['type' => '_parent', 'name' => $parent, 'form_field' => false];
  536                     }
  537                 }
  538             }
  539 
  540             if ($isInputField) {
  541                 $this->parseProperties($key, $properties);
  542             }
  543         }
  544 
  545         if ($isInputField) {
  546             $this->items[$key] = $properties;
  547         }
  548     }
  549 
  550     protected function getFieldKey($key, $prefix, $parent)
  551     {
  552         // Set name from the array key.
  553         if (strpos($key[0], '.') === 0) {
  554             return ($parent ?: rtrim($prefix, '.')) . $key;
  555         }
  556 
  557         return $prefix . $key;
  558     }
  559 
  560     protected function parseProperties($key, array &$properties)
  561     {
  562         $key = ltrim($key, '.');
  563 
  564         if (!empty($properties['data'])) {
  565             $this->dynamic[$key] = $properties['data'];
  566         }
  567 
  568         foreach ($properties as $name => $value) {
  569             if (strpos($name[0], '@') !== false) {
  570                 $list = explode('-', trim($name, '@'), 2);
  571                 $action = array_shift($list);
  572                 $property = array_shift($list);
  573 
  574                 $this->dynamic[$key][$property] = ['action' => $action, 'params' => $value];
  575             }
  576         }
  577 
  578         // Initialize predefined validation rule.
  579         if (isset($properties['validate']['rule'])) {
  580             $properties['validate'] += $this->getRule($properties['validate']['rule']);
  581         }
  582     }
  583 
  584     /**
  585      * Add property to the definition.
  586      *
  587      * @param  string  $path  Comma separated path to the property.
  588      * @internal
  589      */
  590     protected function addProperty($path)
  591     {
  592         $parts = explode('.', $path);
  593         $item = array_pop($parts);
  594 
  595         $nested = &$this->nested;
  596         foreach ($parts as $part) {
  597             if (!isset($nested[$part]) || !\is_array($nested[$part])) {
  598                 $nested[$part] = [];
  599             }
  600 
  601             $nested = &$nested[$part];
  602         }
  603 
  604         if (!isset($nested[$item])) {
  605             $nested[$item] = $path;
  606         }
  607     }
  608 
  609     /**
  610      * Remove property to the definition.
  611      *
  612      * @param  string  $path  Comma separated path to the property.
  613      * @internal
  614      */
  615     protected function removeProperty($path)
  616     {
  617         $parts = explode('.', $path);
  618         $item = array_pop($parts);
  619 
  620         $nested = &$this->nested;
  621         foreach ($parts as $part) {
  622             if (!isset($nested[$part]) || !\is_array($nested[$part])) {
  623                 return;
  624             }
  625 
  626             $nested = &$nested[$part];
  627         }
  628 
  629         if (isset($nested[$item])) {
  630             unset($nested[$item]);
  631         }
  632     }
  633 
  634     /**
  635      * @param $rule
  636      * @return array
  637      * @internal
  638      */
  639     protected function getRule($rule)
  640     {
  641         if (isset($this->rules[$rule]) && \is_array($this->rules[$rule])) {
  642             return $this->rules[$rule];
  643         }
  644         return [];
  645     }
  646 
  647     /**
  648      * @param array $data
  649      * @param array $rules
  650      * @param string $prefix
  651      * @return array
  652      * @internal
  653      */
  654     protected function extraArray(array $data, array $rules, $prefix)
  655     {
  656         $array = [];
  657 
  658         foreach ($data as $key => $field) {
  659             $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
  660             $rule = \is_string($val) ? $this->items[$val] : null;
  661 
  662             if ($rule || isset($val['*'])) {
  663                 // Item has been defined in blueprints.
  664             } elseif (\is_array($field) && \is_array($val)) {
  665                 // Array has been defined in blueprints.
  666                 $array += $this->extraArray($field, $val, $prefix . $key . '.');
  667             } else {
  668                 // Undefined/extra item.
  669                 $array[$prefix.$key] = $field;
  670             }
  671         }
  672         return $array;
  673     }
  674 
  675     /**
  676      * @param array $field
  677      * @param string $property
  678      * @param array $call
  679      */
  680     protected function dynamicData(array &$field, $property, array $call)
  681     {
  682         $params = $call['params'];
  683 
  684         if (\is_array($params)) {
  685             $function = array_shift($params);
  686         } else {
  687             $function = $params;
  688             $params = [];
  689         }
  690 
  691         $list = explode('::', $function, 2);
  692         $f = array_pop($list);
  693         $o = array_pop($list);
  694 
  695         if (!$o) {
  696             if (\function_exists($f)) {
  697                 $data = \call_user_func_array($f, $params);
  698             }
  699         } else {
  700             if (method_exists($o, $f)) {
  701                 $data = \call_user_func_array(array($o, $f), $params);
  702             }
  703         }
  704 
  705         // If function returns a value,
  706         if (isset($data)) {
  707             if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
  708                 // Combine field and @data-field together.
  709                 $field[$property] += $data;
  710             } else {
  711                 // Or create/replace field with @data-field.
  712                 $field[$property] = $data;
  713             }
  714         }
  715     }
  716 }