"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/rockettheme/toolbox/Blueprints/src/BlueprintForm.php" (1 Sep 2020, 16951 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 "BlueprintForm.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 namespace RocketTheme\Toolbox\Blueprints;
    3 
    4 use RocketTheme\Toolbox\ArrayTraits\Export;
    5 use RocketTheme\Toolbox\ArrayTraits\ExportInterface;
    6 use RocketTheme\Toolbox\ArrayTraits\NestedArrayAccessWithGetters;
    7 use RuntimeException;
    8 
    9 /**
   10  * The Config class contains configuration information.
   11  *
   12  * @author RocketTheme
   13  */
   14 abstract class BlueprintForm implements \ArrayAccess, ExportInterface
   15 {
   16     use NestedArrayAccessWithGetters;
   17     use Export;
   18 
   19     /** @var array */
   20     protected $items;
   21 
   22     /** @var string */
   23     protected $filename;
   24 
   25     /** @var string */
   26     protected $context;
   27 
   28     /** @var array */
   29     protected $overrides = [];
   30 
   31     /** @var array */
   32     protected $dynamic = [];
   33 
   34     /**
   35      * Load file and return its contents.
   36      *
   37      * @param string $filename
   38      * @return array
   39      */
   40     abstract protected function loadFile($filename);
   41 
   42     /**
   43      * Get list of blueprint form files (file and its parents for overrides).
   44      *
   45      * @param string|array $path
   46      * @param string $context
   47      * @return array
   48      */
   49     abstract protected function getFiles($path, $context = null);
   50 
   51     /**
   52      * Constructor.
   53      *
   54      * @param string|array $filename
   55      * @param array $items
   56      */
   57     public function __construct($filename = null, array $items = [])
   58     {
   59         $this->nestedSeparator = '/';
   60         $this->filename = $filename;
   61         $this->items = $items;
   62     }
   63 
   64     /**
   65      * Set filename for the blueprint. Can also be array of files for parent lookup.
   66      *
   67      * @param string|array $filename
   68      * @return $this
   69      */
   70     public function setFilename($filename)
   71     {
   72         $this->filename = $filename;
   73 
   74         return $this;
   75     }
   76 
   77     /**
   78      * Get the filename of the blueprint.
   79      *
   80      * @return array|null|string
   81      */
   82     public function getFilename()
   83     {
   84         return $this->filename;
   85     }
   86 
   87     /**
   88      * Set context for import@ and extend@.
   89      *
   90      * @param $context
   91      * @return $this
   92      */
   93     public function setContext($context)
   94     {
   95         $this->context = $context;
   96 
   97         return $this;
   98     }
   99 
  100     /**
  101      * Set custom overrides for import@ and extend@.
  102      *
  103      * @param array $overrides
  104      * @return $this
  105      */
  106     public function setOverrides($overrides)
  107     {
  108         $this->overrides = $overrides;
  109 
  110         return $this;
  111     }
  112 
  113     /**
  114      * Load blueprint.
  115      *
  116      * @return $this
  117      */
  118     public function load($extends = null)
  119     {
  120         // Only load and extend blueprint if it has not yet been loaded.
  121         if (empty($this->items) && $this->filename) {
  122             // Get list of files.
  123             $files = $this->getFiles($this->filename);
  124 
  125             // Load and extend blueprints.
  126             $data = $this->doLoad($files, $extends);
  127 
  128             $this->items = (array) array_shift($data);
  129 
  130             foreach ($data as $content) {
  131                 $this->extend($content, true);
  132             }
  133         }
  134 
  135         // Import blueprints.
  136         $this->deepInit($this->items);
  137 
  138         return $this;
  139     }
  140 
  141     /**
  142      * Initialize blueprints with its dynamic fields.
  143      *
  144      * @return $this
  145      */
  146     public function init()
  147     {
  148         foreach ($this->dynamic as $key => $data) {
  149             // Locate field.
  150             $path = explode('/', $key);
  151             $current = &$this->items;
  152 
  153             foreach ($path as $field) {
  154                 if (\is_object($current)) {
  155                     // Handle objects.
  156                     if (!isset($current->{$field})) {
  157                         $current->{$field} = [];
  158                     }
  159 
  160                     $current = &$current->{$field};
  161                 } else {
  162                     // Handle arrays and scalars.
  163                     if (!\is_array($current)) {
  164                         $current = [$field => []];
  165                     } elseif (!isset($current[$field])) {
  166                         $current[$field] = [];
  167                     }
  168 
  169                     $current = &$current[$field];
  170                 }
  171             }
  172 
  173             // Set dynamic property.
  174             foreach ($data as $property => $call) {
  175                 $action = 'dynamic' . ucfirst($call['action']);
  176 
  177                 if (method_exists($this, $action)) {
  178                     $this->{$action}($current, $property, $call);
  179                 }
  180             }
  181         }
  182 
  183         return $this;
  184     }
  185 
  186 
  187     /**
  188      * Get form.
  189      *
  190      * @return array
  191      */
  192     public function form()
  193     {
  194         return (array) $this->get('form');
  195     }
  196 
  197     /**
  198      * Get form fields.
  199      *
  200      * @return array
  201      */
  202     public function fields()
  203     {
  204         $fields = $this->get('form/fields');
  205 
  206         if ($fields === null) {
  207             $field = $this->get('form/field');
  208             $fields = $field !== null ? ['' => (array) $field] : $fields;
  209         }
  210 
  211         return (array) $fields;
  212     }
  213 
  214     /**
  215      * Extend blueprint with another blueprint.
  216      *
  217      * @param BlueprintForm|array $extends
  218      * @param bool $append
  219      * @return $this
  220      */
  221     public function extend($extends, $append = false)
  222     {
  223         if ($extends instanceof self) {
  224             $extends = $extends->toArray();
  225         }
  226 
  227         if ($append) {
  228             $a = $this->items;
  229             $b = $extends;
  230         } else {
  231             $a = $extends;
  232             $b = $this->items;
  233         }
  234 
  235         $this->items = $this->deepMerge($a, $b);
  236 
  237         return $this;
  238     }
  239 
  240     /**
  241      * @param string $name
  242      * @param mixed $value
  243      * @param string $separator
  244      * @param bool $append
  245      * @return $this
  246      */
  247     public function embed($name, $value, $separator = '/', $append = false)
  248     {
  249         $oldValue = $this->get($name, null, $separator);
  250 
  251         if (\is_array($oldValue) && \is_array($value)) {
  252             if ($append) {
  253                 $a = $oldValue;
  254                 $b = $value;
  255             } else {
  256                 $a = $value;
  257                 $b = $oldValue;
  258             }
  259 
  260             $value = $this->deepMerge($a, $b);
  261         }
  262 
  263         $this->set($name, $value, $separator);
  264 
  265         return $this;
  266     }
  267 
  268     /**
  269      * Get blueprints by using slash notation for nested arrays/objects.
  270      *
  271      * @example $value = $this->resolve('this/is/my/nested/variable');
  272      * returns ['this/is/my', 'nested/variable']
  273      *
  274      * @param array  $path
  275      * @param string  $separator
  276      * @return array
  277      */
  278     public function resolve(array $path, $separator = '/')
  279     {
  280         $fields = false;
  281         $parts = [];
  282         $current = $this['form/fields'];
  283         $result = [null, null, null];
  284 
  285         while (($field = current($path)) !== null) {
  286             if (!$fields && isset($current['fields'])) {
  287                 if (!empty($current['array'])) {
  288                     $result = [$current, $parts, $path ? implode($separator, $path) : null];
  289                     // Skip item offset.
  290                     $parts[] = array_shift($path);
  291                 }
  292 
  293                 $current = $current['fields'];
  294                 $fields = true;
  295 
  296             } elseif (isset($current[$field])) {
  297                 $parts[] = array_shift($path);
  298                 $current = $current[$field];
  299                 $fields = false;
  300 
  301             } elseif (isset($current[$index = '.' . $field])) {
  302                 $parts[] = array_shift($path);
  303                 $current = $current[$index];
  304                 $fields = false;
  305 
  306             } else {
  307                 break;
  308             }
  309         }
  310 
  311         return $result;
  312     }
  313 
  314     /**
  315      * Deep merge two arrays together.
  316      *
  317      * @param array $a
  318      * @param array $b
  319      * @return array
  320      */
  321     protected function deepMerge(array $a, array $b)
  322     {
  323         $bref_stack = [&$a];
  324         $head_stack = [$b];
  325 
  326         do {
  327             end($bref_stack);
  328             $bref = &$bref_stack[key($bref_stack)];
  329             $head = array_pop($head_stack);
  330             unset($bref_stack[key($bref_stack)]);
  331 
  332             foreach ($head as $key => $value) {
  333                 if (strpos($key, '@') !== false) {
  334                     // Remove @ from the start and the end. Key syntax `import@2` is supported to allow multiple operations of the same type.
  335                     $list = explode('-', preg_replace('/^(@*)?([^@]+)(@\d*)?$/', '\2', $key), 2);
  336                     $action = array_shift($list);
  337                     $property = array_shift($list);
  338 
  339                     switch ($action) {
  340                         case 'unset':
  341                         case 'replace':
  342                             if (!$property) {
  343                                 $bref = ['unset@' => true];
  344                             } else {
  345                                 unset($bref[$property]);
  346                             }
  347                             continue 2;
  348                     }
  349                 }
  350 
  351                 if (isset($key, $bref[$key]) && \is_array($bref[$key]) && \is_array($head[$key])) {
  352                     $bref_stack[] = &$bref[$key];
  353                     $head_stack[] = $head[$key];
  354                 } else {
  355                     $bref = array_merge($bref, [$key => $head[$key]]);
  356                 }
  357             }
  358         } while (\count($head_stack));
  359 
  360         return $a;
  361     }
  362 
  363     /**
  364      * @param array $items
  365      * @param array $path
  366      * @return string
  367      */
  368     protected function deepInit(array &$items, $path = [])
  369     {
  370         $ordering = '';
  371         $order = [];
  372         $field = end($path) === 'fields';
  373 
  374         foreach ($items as $key => &$item) {
  375             // Set name for nested field.
  376             if ($field && isset($item['type'])) {
  377                 $item['name'] = $key;
  378             }
  379 
  380             // Handle special instructions in the form.
  381             if (strpos($key, '@') !== false) {
  382                 // Remove @ from the start and the end. Key syntax `import@2` is supported to allow multiple operations of the same type.
  383                 $list = explode('-', preg_replace('/^(@*)?([^@]+)(@\d*)?$/', '\2', $key), 2);
  384                 $action = array_shift($list);
  385                 $property = array_shift($list);
  386 
  387                 switch ($action) {
  388                     case 'unset':
  389                         unset($items[$key]);
  390                         if (empty($items)) {
  391                             return null;
  392                         }
  393                         break;
  394                     case 'import':
  395                         unset($items[$key]);
  396                         $this->doImport($item, $path);
  397                         break;
  398                     case 'ordering':
  399                         $ordering = $item;
  400                         unset($items[$key]);
  401                         break;
  402                     default:
  403                         $this->dynamic[implode('/', $path)][$property] = ['action' => $action, 'params' => $item];
  404                 }
  405 
  406             } elseif (\is_array($item)) {
  407                 // Recursively initialize form.
  408                 $newPath = array_merge($path, [$key]);
  409 
  410                 $location = $this->deepInit($item, $newPath);
  411                 if ($location) {
  412                     $order[$key] = $location;
  413                 } elseif ($location === null) {
  414                     unset($items[$key]);
  415                 }
  416             }
  417         }
  418         unset($item);
  419 
  420         if ($order) {
  421             // Reorder fields if needed.
  422             $items = $this->doReorder($items, $order);
  423         }
  424 
  425         return $ordering;
  426     }
  427 
  428     /**
  429      * @param array|string $value
  430      * @return array|null
  431      */
  432     protected function loadImport($value)
  433     {
  434         $type = !\is_string($value) ? (!isset($value['type']) ? null : $value['type']) : $value;
  435         $field = 'form';
  436 
  437         if ($type && strpos($type, ':') !== false) {
  438             list ($type, $field) = explode(':', $type, 2);
  439         }
  440 
  441         if (!$type && !$field) {
  442             return null;
  443         }
  444 
  445         if ($type) {
  446             $files = $this->getFiles($type, isset($value['context']) ? $value['context'] : null);
  447 
  448             if (!$files) {
  449                 return null;
  450             }
  451 
  452             /** @var BlueprintForm $blueprint */
  453             $blueprint = new static($files);
  454             $blueprint->setContext($this->context)->setOverrides($this->overrides)->load();
  455         } else {
  456             $blueprint = $this;
  457         }
  458 
  459         $import = $blueprint->get($field);
  460 
  461         return \is_array($import) ? $import : null;
  462     }
  463 
  464     /**
  465      * @param array|string $value
  466      * @param array $path
  467      */
  468     protected function doImport($value, array &$path)
  469     {
  470         $imported = $this->loadImport($value);
  471 
  472         if ($imported) {
  473             $this->deepInit($imported, $path);
  474             $name = implode('/', $path);
  475             $this->embed($name, $imported, '/', false);
  476         }
  477     }
  478 
  479     /**
  480      * Internal function that handles loading extended blueprints.
  481      *
  482      * @param array $files
  483      * @param string|array|null $extends
  484      * @return array
  485      */
  486     protected function doLoad(array $files, $extends = null)
  487     {
  488         $filename = array_shift($files);
  489         $content = $this->loadFile($filename);
  490 
  491         $key = '';
  492         if (isset($content['extends@'])) {
  493             $key = 'extends@';
  494         } elseif (isset($content['@extends'])) {
  495             $key = '@extends';
  496         } elseif (isset($content['@extends@'])) {
  497             $key = '@extends@';
  498         }
  499 
  500         $override = (bool)$extends;
  501         $extends = (array)($key && !$extends ? $content[$key] : $extends);
  502 
  503         unset($content['extends@'], $content['@extends'], $content['@extends@']);
  504 
  505         $data = $extends ? $this->doExtend($filename, $files, $extends, $override) : [];
  506         $data[] = $content;
  507 
  508         return $data;
  509     }
  510 
  511     /**
  512      * Internal function to recursively load extended blueprints.
  513      *
  514      * @param string $filename
  515      * @param array $parents
  516      * @param array $extends
  517      * @return array
  518      */
  519     protected function doExtend($filename, array $parents, array $extends, $override = false)
  520     {
  521         if (\is_string(key($extends))) {
  522             $extends = [$extends];
  523         }
  524 
  525         $data = [[]];
  526         foreach ($extends as $value) {
  527             // Accept array of type and context or a string.
  528             $type = !\is_string($value) ? (!isset($value['type']) ? null : $value['type']) : $value;
  529 
  530             if (!$type) {
  531                 continue;
  532             }
  533 
  534             if ($type === '@parent' || $type === 'parent@') {
  535                 if (!$parents) {
  536                     throw new RuntimeException("Parent blueprint missing for '{$filename}'");
  537                 }
  538 
  539                 $files = $parents;
  540             } else {
  541                 $files = $this->getFiles($type, isset($value['context']) ? $value['context'] : null);
  542 
  543                 if ($override && !$files) {
  544                     throw new RuntimeException("Blueprint '{$type}' missing for '{$filename}'");
  545                 }
  546 
  547                 // Detect extend loops.
  548                 if ($files && array_intersect($files, $parents)) {
  549                     // Let's check if user really meant extends@: parent@.
  550                     $index = \array_search($filename, $files, true);
  551                     if ($index !== false) {
  552                         // We want to grab only the parents of the file which is currently being loaded.
  553                         $files = \array_slice($files, $index + 1);
  554                     }
  555                     if ($files !== $parents) {
  556                         throw new RuntimeException("Loop detected while extending blueprint file '{$filename}'");
  557                     }
  558                     if (!$parents) {
  559                         throw new RuntimeException("Parent blueprint missing for '{$filename}'");
  560                     }
  561                 }
  562             }
  563 
  564             if ($files) {
  565                 $data[] = $this->doLoad($files);
  566             }
  567         }
  568 
  569         // TODO: In PHP 5.6+ use array_merge(...$data);
  570         return call_user_func_array('array_merge', $data);
  571     }
  572 
  573     /**
  574      * Internal function to reorder items.
  575      *
  576      * @param array $items
  577      * @param array $keys
  578      * @return array
  579      */
  580     protected function doReorder(array $items, array $keys)
  581     {
  582         $reordered = array_keys($items);
  583 
  584         foreach ($keys as $item => $ordering) {
  585             if ((string)(int) $ordering === (string) $ordering) {
  586                 $location = array_search($item, $reordered, true);
  587                 $rel = array_splice($reordered, $location, 1);
  588                 array_splice($reordered, $ordering, 0, $rel);
  589 
  590             } elseif (isset($items[$ordering])) {
  591                 $location = array_search($item, $reordered, true);
  592                 $rel = array_splice($reordered, $location, 1);
  593                 $location = array_search($ordering, $reordered, true);
  594                 array_splice($reordered, $location + 1, 0, $rel);
  595             }
  596         }
  597 
  598         return array_merge(array_flip($reordered), $items);
  599     }
  600 }