wesmiler 2 éve
szülő
commit
1c855a51c7

+ 1 - 0
vendor/adbario/php-dot-notation/.coveralls.yml

@@ -0,0 +1 @@
+json_path: coveralls-upload.json

+ 10 - 0
vendor/adbario/php-dot-notation/.editorconfig

@@ -0,0 +1,10 @@
+[*]
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml]
+indent_size = 2

+ 9 - 0
vendor/adbario/php-dot-notation/.gitattributes

@@ -0,0 +1,9 @@
+/.coveralls.yml export-ignore
+/.editorconfig export-ignore
+/.github export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/phpcs.xml export-ignore
+/phpunit.xml.dist export-ignore
+/README.md export-ignore
+/tests export-ignore

+ 44 - 0
vendor/adbario/php-dot-notation/.github/workflows/tests.yml

@@ -0,0 +1,44 @@
+name: tests
+
+on: [push, pull_request]
+
+jobs:
+  tests:
+    name: Test PHP ${{ matrix.php }}
+    runs-on: ubuntu-latest
+    continue-on-error: ${{ matrix.experimental }}
+    strategy:
+      matrix:
+        php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
+        experimental: [false]
+        include:
+          - php: 8.1
+            analysis: true
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Set up PHP ${{ matrix.php }}
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          coverage: xdebug
+
+      - name: Install dependencies with Composer
+        run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+      - name: Coding standards
+        if: matrix.analysis
+        run: vendor/bin/phpcs
+
+      - name: Tests
+        run: vendor/bin/phpunit --coverage-clover clover.xml
+
+      - name: Upload coverage results to Coveralls
+        if: matrix.analysis
+        env:
+          COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          composer require php-coveralls/php-coveralls -n -W
+          vendor/bin/php-coveralls --coverage_clover=clover.xml -v

+ 9 - 0
vendor/adbario/php-dot-notation/.gitignore

@@ -0,0 +1,9 @@
+.DS_Store
+.idea
+.phpunit.result.cache
+.vscode
+clover.xml
+composer.lock
+phpunit.xml
+vendor
+coverage

+ 21 - 0
vendor/adbario/php-dot-notation/LICENSE.md

@@ -0,0 +1,21 @@
+# The MIT License (MIT)
+
+Copyright (c) 2016-2019 Riku Särkinen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 442 - 0
vendor/adbario/php-dot-notation/README.md

@@ -0,0 +1,442 @@
+# Dot - PHP dot notation access to arrays
+
+<div>
+  <a href="https://github.com/adbario/php-dot-notation/actions/workflows/tests.yml?query=branch%3A2.x"><img src="https://img.shields.io/github/workflow/status/adbario/php-dot-notation/tests/2.x?label=tests&style=for-the-badge" alt="Build Status"></a>
+  <a href="https://coveralls.io/github/adbario/php-dot-notation?branch=2.x"><img src="https://img.shields.io/coveralls/github/adbario/php-dot-notation/2.x?style=for-the-badge" alt="Coverage Status"></a>
+  <a href="https://packagist.org/packages/adbario/php-dot-notation"><img src="https://img.shields.io/packagist/dt/adbario/php-dot-notation?style=for-the-badge" alt="Total Downloads"></a>
+  <a href="https://packagist.org/packages/adbario/php-dot-notation"><img src="https://img.shields.io/packagist/v/adbario/php-dot-notation?label=stable&style=for-the-badge" alt="Latest Stable Version"></a>
+  <a href="LICENSE.md"><img src="https://img.shields.io/packagist/l/adbario/php-dot-notation?style=for-the-badge" alt="License"></a>
+</div>
+
+Dot provides an easy access to arrays of data with dot notation in a lightweight and fast way. Inspired by Laravel Collection.
+
+Dot implements PHP's ArrayAccess interface and Dot object can also be used the same way as normal arrays with additional dot notation.
+
+## Examples
+
+With Dot you can change this regular array syntax:
+
+```php
+$array['info']['home']['address'] = 'Kings Square';
+
+echo $array['info']['home']['address'];
+
+// Kings Square
+```
+
+to this (Dot object):
+
+```php
+$dot->set('info.home.address', 'Kings Square');
+
+echo $dot->get('info.home.address');
+```
+
+or even this (ArrayAccess):
+
+```php
+$dot['info.home.address'] = 'Kings Square';
+
+echo $dot['info.home.address'];
+```
+
+## Install
+
+Install the latest version using [Composer](https://getcomposer.org/):
+
+```
+$ composer require adbario/php-dot-notation
+```
+
+## Usage
+
+Create a new Dot object:
+
+```php
+$dot = new \Adbar\Dot;
+
+// With existing array
+$dot = new \Adbar\Dot($array);
+```
+
+You can also use a helper function to create the object:
+```php
+$dot = dot();
+
+// With existing array
+$dot = dot($array);
+```
+
+It is possible use an alternative delimiter from the default dot (`.`) with the second constructor parameter.  Using an underscore instead:
+
+```php
+$dot = new \Adbar\Dot($array, '_');
+
+// With the helper
+$dot = dot($array, '_');
+```
+
+## Methods
+
+Dot has the following methods:
+
+- [add()](#add)
+- [all()](#all)
+- [clear()](#clear)
+- [count()](#count)
+- [delete()](#delete)
+- [flatten()](#flatten)
+- [get()](#get)
+- [has()](#has)
+- [isEmpty()](#isempty)
+- [merge()](#merge)
+- [mergeRecursive()](#mergerecursive)
+- [mergeRecursiveDistinct()](#mergerecursivedistinct)
+- [pull()](#pull)
+- [push()](#push)
+- [replace()](#replace)
+- [set()](#set)
+- [setArray()](#setarray)
+- [setReference()](#setreference)
+- [toJson()](#tojson)
+
+<a name="add"></a>
+### add()
+
+Sets a given key / value pair if the key doesn't exist already:
+```php
+$dot->add('user.name', 'John');
+
+// Equivalent vanilla PHP
+if (!isset($array['user']['name'])) {
+    $array['user']['name'] = 'John';
+}
+```
+
+Multiple key / value pairs:
+```php
+$dot->add([
+    'user.name' => 'John',
+    'page.title' => 'Home'
+]);
+```
+
+<a name="all"></a>
+### all()
+
+Returns all the stored items as an array:
+```php
+$values = $dot->all();
+```
+
+<a name="clear"></a>
+### clear()
+
+Deletes the contents of a given key (sets an empty array):
+```php
+$dot->clear('user.settings');
+
+// Equivalent vanilla PHP
+$array['user']['settings'] = [];
+```
+
+Multiple keys:
+```php
+$dot->clear(['user.settings', 'app.config']);
+```
+
+All the stored items:
+```php
+$dot->clear();
+
+// Equivalent vanilla PHP
+$array = [];
+```
+
+<a name="count"></a>
+### count()
+
+Returns the number of items in a given key:
+```php
+$dot->count('user.siblings');
+```
+
+Items in the root of Dot object:
+```php
+$dot->count();
+
+// Or use coun() function as Dot implements Countable
+count($dot);
+```
+
+<a name="delete"></a>
+### delete()
+
+Deletes the given key:
+```php
+$dot->delete('user.name');
+
+// ArrayAccess
+unset($dot['user.name']);
+
+// Equivalent vanilla PHP
+unset($array['user']['name']);
+```
+
+Multiple keys:
+```php
+$dot->delete([
+    'user.name',
+    'page.title'
+]);
+```
+
+<a name="flatten"></a>
+### flatten()
+
+Returns a flattened array with the keys delimited by a given character (default "."):
+```php
+$flatten = $dot->flatten();
+```
+
+<a name="get"></a>
+### get()
+
+Returns the value of a given key:
+```php
+echo $dot->get('user.name');
+
+// ArrayAccess
+echo $dot['user.name'];
+
+// Equivalent vanilla PHP < 7.0
+echo isset($array['user']['name']) ? $array['user']['name'] : null;
+
+// Equivalent vanilla PHP >= 7.0
+echo $array['user']['name'] ?? null;
+```
+
+Returns a given default value, if the given key doesn't exist:
+```php
+echo $dot->get('user.name', 'some default value');
+```
+
+<a name="has"></a>
+### has()
+
+Checks if a given key exists (returns boolean true or false):
+```php
+$dot->has('user.name');
+
+// ArrayAccess
+isset($dot['user.name']);
+```
+
+Multiple keys:
+```php
+$dot->has([
+    'user.name',
+    'page.title'
+]);
+```
+
+<a name="isempty"></a>
+### isEmpty()
+
+Checks if a given key is empty (returns boolean true or false):
+```php
+$dot->isEmpty('user.name');
+
+// ArrayAccess
+empty($dot['user.name']);
+
+// Equivalent vanilla PHP
+empty($array['user']['name']);
+```
+
+Multiple keys:
+```php
+$dot->isEmpty([
+    'user.name',
+    'page.title'
+]);
+```
+
+Checks the whole Dot object:
+```php
+$dot->isEmpty();
+```
+
+<a name="merge"></a>
+### merge()
+
+Merges a given array or another Dot object:
+```php
+$dot->merge($array);
+
+// Equivalent vanilla PHP
+array_merge($originalArray, $array);
+```
+
+Merges a given array or another Dot object with the given key:
+```php
+$dot->merge('user', $array);
+
+// Equivalent vanilla PHP
+array_merge($originalArray['user'], $array);
+```
+
+<a name="mergerecursive"></a>
+### mergeRecursive()
+
+Recursively merges a given array or another Dot object:
+```php
+$dot->mergeRecursive($array);
+
+// Equivalent vanilla PHP
+array_merge_recursive($originalArray, $array);
+```
+
+Recursively merges a given array or another Dot object with the given key:
+```php
+$dot->mergeRecursive('user', $array);
+
+// Equivalent vanilla PHP
+array_merge_recursive($originalArray['user'], $array);
+```
+
+<a name="mergerecursivedistinct"></a>
+### mergeRecursiveDistinct()
+
+Recursively merges a given array or another Dot object. Duplicate keys overwrite the value in the
+original array (unlike [mergeRecursiveDistinct()](#mergerecursivedistinct), where duplicate keys are transformed
+into arrays with multiple values):
+```php
+$dot->mergeRecursiveDistinct($array);
+```
+
+Recursively merges a given array or another Dot object with the given key. Duplicate keys overwrite the value in the
+original array.
+```php
+$dot->mergeRecursiveDistinct('user', $array);
+```
+
+<a name="pull"></a>
+### pull()
+
+Returns the value of a given key and deletes the key:
+```php
+echo $dot->pull('user.name');
+
+// Equivalent vanilla PHP < 7.0
+echo isset($array['user']['name']) ? $array['user']['name'] : null;
+unset($array['user']['name']);
+
+// Equivalent vanilla PHP >= 7.0
+echo $array['user']['name'] ?? null;
+unset($array['user']['name']);
+```
+
+Returns a given default value, if the given key doesn't exist:
+```php
+echo $dot->pull('user.name', 'some default value');
+```
+
+Returns all the stored items as an array and clears the Dot object:
+```php
+$items = $dot->pull();
+```
+
+<a name="push"></a>
+### push()
+
+Pushes a given value to the end of the array in a given key:
+```php
+$dot->push('users', 'John');
+
+// Equivalent vanilla PHP
+$array['users'][] = 'John';
+```
+
+Pushes a given value to the end of the array:
+```php
+$dot->push('John');
+
+// Equivalent vanilla PHP
+$array[] = 'John';
+```
+
+<a name="replace"></a>
+### replace()
+
+Replaces the values with values having the same keys in the given array or Dot object:
+```php
+$dot->replace($array);
+
+// Equivalent vanilla PHP
+array_replace($originalArray, $array);
+```
+
+Replaces the values with values having the same keys in the given array or Dot object with the given key:
+```php
+$dot->merge('user', $array);
+
+// Equivalent vanilla PHP
+array_replace($originalArray['user'], $array);
+```
+`replace()` is not recursive.
+
+<a name="set"></a>
+### set()
+
+Sets a given key / value pair:
+```php
+$dot->set('user.name', 'John');
+
+// ArrayAccess
+$dot['user.name'] = 'John';
+
+// Equivalent vanilla PHP
+$array['user']['name'] = 'John';
+```
+
+Multiple key / value pairs:
+```php
+$dot->set([
+    'user.name' => 'John',
+    'page.title'     => 'Home'
+]);
+```
+
+<a name="setarray"></a>
+### setArray()
+
+Replaces all items in Dot object with a given array:
+```php
+$dot->setArray($array);
+```
+
+<a name="setreference"></a>
+### setReference()
+
+Replaces all items in Dot object with a given array as a reference and all future changes to Dot will be made directly to the original array:
+```php
+$dot->setReference($array);
+```
+
+<a name="tojson"></a>
+### toJson()
+
+Returns the value of a given key as JSON:
+```php
+echo $dot->toJson('user');
+```
+
+Returns all the stored items as JSON:
+```php
+echo $dot->toJson();
+```
+
+## License
+
+[MIT license](LICENSE.md)

+ 29 - 0
vendor/adbario/php-dot-notation/composer.json

@@ -0,0 +1,29 @@
+{
+    "name": "adbario/php-dot-notation",
+    "description": "PHP dot notation access to arrays",
+    "keywords": ["dotnotation", "arrayaccess"],
+    "homepage": "https://github.com/adbario/php-dot-notation",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Riku Särkinen",
+            "email": "riku@adbar.io"
+        }
+    ],
+    "require": {
+        "php": "^5.5 || ^7.0 || ^8.0",
+        "ext-json": "*"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^4.8|^5.7|^6.6|^7.5|^8.5|^9.5",
+        "squizlabs/php_codesniffer": "^3.6"
+    },
+    "autoload": {
+        "files": [
+            "src/helpers.php"
+        ],
+        "psr-4": {
+            "Adbar\\": "src"
+        }
+    }
+}

+ 18 - 0
vendor/adbario/php-dot-notation/phpcs.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<ruleset name="Dot">
+    <description>Dot coding standard</description>
+
+    <!-- Output -->
+    <arg value="p" />
+    <arg name="colors" />
+
+    <!-- Rules -->
+    <rule ref="PSR2" />
+
+    <!-- File extensions -->
+    <arg name="extensions" value="php" />
+
+    <!-- Include paths -->
+    <file>src</file>
+    <file>tests</file>
+</ruleset>

+ 26 - 0
vendor/adbario/php-dot-notation/phpunit.xml.dist

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.0/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         backupGlobals="false"
+         colors="true"
+         verbose="true"
+         stopOnFailure="false">
+
+    <testsuites>
+        <testsuite name="Dot Test Suite">
+            <directory>tests</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+
+    <logging>
+        <!-- <log type="coverage-html" target="build/coverage" /> -->
+        <!-- <log type="coverage-text" target="php://stdout" /> -->
+    </logging>
+</phpunit>

+ 623 - 0
vendor/adbario/php-dot-notation/src/Dot.php

@@ -0,0 +1,623 @@
+<?php
+/**
+ * Dot - PHP dot notation access to arrays
+ *
+ * @author  Riku Särkinen <riku@adbar.io>
+ * @link    https://github.com/adbario/php-dot-notation
+ * @license https://github.com/adbario/php-dot-notation/blob/2.x/LICENSE.md (MIT License)
+ */
+namespace Adbar;
+
+use Countable;
+use ArrayAccess;
+use ArrayIterator;
+use JsonSerializable;
+use IteratorAggregate;
+
+/**
+ * Dot
+ *
+ * This class provides a dot notation access and helper functions for
+ * working with arrays of data. Inspired by Laravel Collection.
+ */
+class Dot implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
+{
+    /**
+     * The stored items
+     *
+     * @var array
+     */
+    protected $items = [];
+
+
+    /**
+     * The delimiter (alternative to a '.') to be used.
+     *
+     * @var string
+     */
+    protected $delimiter = '.';
+
+
+    /**
+     * Create a new Dot instance
+     *
+     * @param mixed $items
+     * @param string $delimiter
+     */
+    public function __construct($items = [], $delimiter = '.')
+    {
+        $this->items = $this->getArrayItems($items);
+        $this->delimiter = strlen($delimiter) ? $delimiter : '.';
+    }
+
+    /**
+     * Set a given key / value pair or pairs
+     * if the key doesn't exist already
+     *
+     * @param array|int|string $keys
+     * @param mixed            $value
+     */
+    public function add($keys, $value = null)
+    {
+        if (is_array($keys)) {
+            foreach ($keys as $key => $value) {
+                $this->add($key, $value);
+            }
+        } elseif (is_null($this->get($keys))) {
+            $this->set($keys, $value);
+        }
+    }
+
+    /**
+     * Return all the stored items
+     *
+     * @return array
+     */
+    public function all()
+    {
+        return $this->items;
+    }
+
+    /**
+     * Delete the contents of a given key or keys
+     *
+     * @param array|int|string|null $keys
+     */
+    public function clear($keys = null)
+    {
+        if (is_null($keys)) {
+            $this->items = [];
+
+            return;
+        }
+
+        $keys = (array) $keys;
+
+        foreach ($keys as $key) {
+            $this->set($key, []);
+        }
+    }
+
+    /**
+     * Delete the given key or keys
+     *
+     * @param array|int|string $keys
+     */
+    public function delete($keys)
+    {
+        $keys = (array) $keys;
+
+        foreach ($keys as $key) {
+            if ($this->exists($this->items, $key)) {
+                unset($this->items[$key]);
+
+                continue;
+            }
+
+            $items = &$this->items;
+            $segments = explode($this->delimiter, $key);
+            $lastSegment = array_pop($segments);
+
+            foreach ($segments as $segment) {
+                if (!isset($items[$segment]) || !is_array($items[$segment])) {
+                    continue 2;
+                }
+
+                $items = &$items[$segment];
+            }
+
+            unset($items[$lastSegment]);
+        }
+    }
+
+    /**
+     * Checks if the given key exists in the provided array.
+     *
+     * @param  array      $array Array to validate
+     * @param  int|string $key   The key to look for
+     *
+     * @return bool
+     */
+    protected function exists($array, $key)
+    {
+        return array_key_exists($key, $array);
+    }
+
+    /**
+     * Flatten an array with the given character as a key delimiter
+     *
+     * @param  string     $delimiter
+     * @param  array|null $items
+     * @param  string     $prepend
+     * @return array
+     */
+    public function flatten($delimiter = '.', $items = null, $prepend = '')
+    {
+        $flatten = [];
+
+        if (is_null($items)) {
+            $items = $this->items;
+        }
+
+        if (!func_num_args()) {
+            $delimiter = $this->delimiter;
+        }
+
+        foreach ($items as $key => $value) {
+            if (is_array($value) && !empty($value)) {
+                $flatten = array_merge(
+                    $flatten,
+                    $this->flatten($delimiter, $value, $prepend.$key.$delimiter)
+                );
+            } else {
+                $flatten[$prepend.$key] = $value;
+            }
+        }
+
+        return $flatten;
+    }
+
+    /**
+     * Return the value of a given key
+     *
+     * @param  int|string|null $key
+     * @param  mixed           $default
+     * @return mixed
+     */
+    public function get($key = null, $default = null)
+    {
+        if (is_null($key)) {
+            return $this->items;
+        }
+
+        if ($this->exists($this->items, $key)) {
+            return $this->items[$key];
+        }
+
+        if (strpos($key, $this->delimiter) === false) {
+            return $default;
+        }
+
+        $items = $this->items;
+
+        foreach (explode($this->delimiter, $key) as $segment) {
+            if (!is_array($items) || !$this->exists($items, $segment)) {
+                return $default;
+            }
+
+            $items = &$items[$segment];
+        }
+
+        return $items;
+    }
+
+    /**
+     * Return the given items as an array
+     *
+     * @param  mixed $items
+     * @return array
+     */
+    protected function getArrayItems($items)
+    {
+        if (is_array($items)) {
+            return $items;
+        } elseif ($items instanceof self) {
+            return $items->all();
+        }
+
+        return (array) $items;
+    }
+
+    /**
+     * Check if a given key or keys exists
+     *
+     * @param  array|int|string $keys
+     * @return bool
+     */
+    public function has($keys)
+    {
+        $keys = (array) $keys;
+
+        if (!$this->items || $keys === []) {
+            return false;
+        }
+
+        foreach ($keys as $key) {
+            $items = $this->items;
+
+            if ($this->exists($items, $key)) {
+                continue;
+            }
+
+            foreach (explode($this->delimiter, $key) as $segment) {
+                if (!is_array($items) || !$this->exists($items, $segment)) {
+                    return false;
+                }
+
+                $items = $items[$segment];
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check if a given key or keys are empty
+     *
+     * @param  array|int|string|null $keys
+     * @return bool
+     */
+    public function isEmpty($keys = null)
+    {
+        if (is_null($keys)) {
+            return empty($this->items);
+        }
+
+        $keys = (array) $keys;
+
+        foreach ($keys as $key) {
+            if (!empty($this->get($key))) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Merge a given array or a Dot object with the given key
+     * or with the whole Dot object
+     *
+     * @param array|string|self $key
+     * @param array|self        $value
+     */
+    public function merge($key, $value = [])
+    {
+        if (is_array($key)) {
+            $this->items = array_merge($this->items, $key);
+        } elseif (is_string($key)) {
+            $items = (array) $this->get($key);
+            $value = array_merge($items, $this->getArrayItems($value));
+
+            $this->set($key, $value);
+        } elseif ($key instanceof self) {
+            $this->items = array_merge($this->items, $key->all());
+        }
+    }
+
+    /**
+     * Recursively merge a given array or a Dot object with the given key
+     * or with the whole Dot object.
+     *
+     * Duplicate keys are converted to arrays.
+     *
+     * @param array|string|self $key
+     * @param array|self        $value
+     */
+    public function mergeRecursive($key, $value = [])
+    {
+        if (is_array($key)) {
+            $this->items = array_merge_recursive($this->items, $key);
+        } elseif (is_string($key)) {
+            $items = (array) $this->get($key);
+            $value = array_merge_recursive($items, $this->getArrayItems($value));
+
+            $this->set($key, $value);
+        } elseif ($key instanceof self) {
+            $this->items = array_merge_recursive($this->items, $key->all());
+        }
+    }
+
+    /**
+     * Recursively merge a given array or a Dot object with the given key
+     * or with the whole Dot object.
+     *
+     * Instead of converting duplicate keys to arrays, the value from
+     * given array will replace the value in Dot object.
+     *
+     * @param array|string|self $key
+     * @param array|self        $value
+     */
+    public function mergeRecursiveDistinct($key, $value = [])
+    {
+        if (is_array($key)) {
+            $this->items = $this->arrayMergeRecursiveDistinct($this->items, $key);
+        } elseif (is_string($key)) {
+            $items = (array) $this->get($key);
+            $value = $this->arrayMergeRecursiveDistinct($items, $this->getArrayItems($value));
+
+            $this->set($key, $value);
+        } elseif ($key instanceof self) {
+            $this->items = $this->arrayMergeRecursiveDistinct($this->items, $key->all());
+        }
+    }
+
+    /**
+     * Merges two arrays recursively. In contrast to array_merge_recursive,
+     * duplicate keys are not converted to arrays but rather overwrite the
+     * value in the first array with the duplicate value in the second array.
+     *
+     * @param  array $array1 Initial array to merge
+     * @param  array $array2 Array to recursively merge
+     * @return array
+     */
+    protected function arrayMergeRecursiveDistinct(array $array1, array $array2)
+    {
+        $merged = &$array1;
+
+        foreach ($array2 as $key => $value) {
+            if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
+                $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value);
+            } else {
+                $merged[$key] = $value;
+            }
+        }
+
+        return $merged;
+    }
+
+    /**
+     * Return the value of a given key and
+     * delete the key
+     *
+     * @param  int|string|null $key
+     * @param  mixed           $default
+     * @return mixed
+     */
+    public function pull($key = null, $default = null)
+    {
+        if (is_null($key)) {
+            $value = $this->all();
+            $this->clear();
+
+            return $value;
+        }
+
+        $value = $this->get($key, $default);
+        $this->delete($key);
+
+        return $value;
+    }
+
+    /**
+     * Push a given value to the end of the array
+     * in a given key
+     *
+     * @param mixed $key
+     * @param mixed $value
+     */
+    public function push($key, $value = null)
+    {
+        if (is_null($value)) {
+            $this->items[] = $key;
+
+            return;
+        }
+
+        $items = $this->get($key);
+
+        if (is_array($items) || is_null($items)) {
+            $items[] = $value;
+            $this->set($key, $items);
+        }
+    }
+
+    /**
+     * Replace all values or values within the given key
+     * with an array or Dot object
+     *
+     * @param array|string|self $key
+     * @param array|self        $value
+     */
+    public function replace($key, $value = [])
+    {
+        if (is_array($key)) {
+            $this->items = array_replace($this->items, $key);
+        } elseif (is_string($key)) {
+            $items = (array) $this->get($key);
+            $value = array_replace($items, $this->getArrayItems($value));
+
+            $this->set($key, $value);
+        } elseif ($key instanceof self) {
+            $this->items = array_replace($this->items, $key->all());
+        }
+    }
+
+    /**
+     * Set a given key / value pair or pairs
+     *
+     * @param array|int|string $keys
+     * @param mixed            $value
+     */
+    public function set($keys, $value = null)
+    {
+        if (is_array($keys)) {
+            foreach ($keys as $key => $value) {
+                $this->set($key, $value);
+            }
+
+            return;
+        }
+
+        $items = &$this->items;
+
+        foreach (explode($this->delimiter, $keys) as $key) {
+            if (!isset($items[$key]) || !is_array($items[$key])) {
+                $items[$key] = [];
+            }
+
+            $items = &$items[$key];
+        }
+
+        $items = $value;
+    }
+
+    /**
+     * Replace all items with a given array
+     *
+     * @param mixed $items
+     */
+    public function setArray($items)
+    {
+        $this->items = $this->getArrayItems($items);
+    }
+
+    /**
+     * Replace all items with a given array as a reference
+     *
+     * @param array $items
+     */
+    public function setReference(array &$items)
+    {
+        $this->items = &$items;
+    }
+
+    /**
+     * Return the value of a given key or all the values as JSON
+     *
+     * @param  mixed  $key
+     * @param  int    $options
+     * @return string
+     */
+    public function toJson($key = null, $options = 0)
+    {
+        if (is_string($key)) {
+            return json_encode($this->get($key), $options);
+        }
+
+        $options = $key === null ? 0 : $key;
+
+        return json_encode($this->items, $options);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * ArrayAccess interface
+     * --------------------------------------------------------------
+     */
+
+    /**
+     * Check if a given key exists
+     *
+     * @param  int|string $key
+     * @return bool
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetExists($key)
+    {
+        return $this->has($key);
+    }
+
+    /**
+     * Return the value of a given key
+     *
+     * @param  int|string $key
+     * @return mixed
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetGet($key)
+    {
+        return $this->get($key);
+    }
+
+    /**
+     * Set a given value to the given key
+     *
+     * @param int|string|null $key
+     * @param mixed           $value
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetSet($key, $value)
+    {
+        if (is_null($key)) {
+            $this->items[] = $value;
+
+            return;
+        }
+
+        $this->set($key, $value);
+    }
+
+    /**
+     * Delete the given key
+     *
+     * @param int|string $key
+     */
+    #[\ReturnTypeWillChange]
+    public function offsetUnset($key)
+    {
+        $this->delete($key);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Countable interface
+     * --------------------------------------------------------------
+     */
+
+    /**
+     * Return the number of items in a given key
+     *
+     * @param  int|string|null $key
+     * @return int
+     */
+    #[\ReturnTypeWillChange]
+    public function count($key = null)
+    {
+        return count($this->get($key));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * IteratorAggregate interface
+     * --------------------------------------------------------------
+     */
+
+     /**
+     * Get an iterator for the stored items
+     *
+     * @return \ArrayIterator
+     */
+    #[\ReturnTypeWillChange]
+    public function getIterator()
+    {
+        return new ArrayIterator($this->items);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * JsonSerializable interface
+     * --------------------------------------------------------------
+     */
+
+    /**
+     * Return items for JSON serialization
+     *
+     * @return array
+     */
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+        return $this->items;
+    }
+}

+ 24 - 0
vendor/adbario/php-dot-notation/src/helpers.php

@@ -0,0 +1,24 @@
+<?php
+/**
+ * Dot - PHP dot notation access to arrays
+ *
+ * @author  Riku Särkinen <riku@adbar.io>
+ * @link    https://github.com/adbario/php-dot-notation
+ * @license https://github.com/adbario/php-dot-notation/blob/2.x/LICENSE.md (MIT License)
+ */
+
+use Adbar\Dot;
+
+if (! function_exists('dot')) {
+    /**
+     * Create a new Dot object with the given items and optional delimiter
+     *
+     * @param  mixed  $items
+     * @param  string $delimiter
+     * @return \Adbar\Dot
+     */
+    function dot($items, $delimiter = '.')
+    {
+        return new Dot($items, $delimiter);
+    }
+}

+ 769 - 0
vendor/adbario/php-dot-notation/tests/DotTest.php

@@ -0,0 +1,769 @@
+<?php
+/**
+ * Dot - PHP dot notation access to arrays
+ *
+ * @author  Riku Särkinen <riku@adbar.io>
+ * @link    https://github.com/adbario/php-dot-notation
+ * @license https://github.com/adbario/php-dot-notation/blob/2.x/LICENSE.md (MIT License)
+ */
+namespace Adbar\Tests;
+
+use Adbar\Dot;
+use ArrayIterator;
+use PHPUnit\Framework\TestCase;
+
+class DotTest extends TestCase
+{
+    /*
+     * --------------------------------------------------------------
+     * Construct
+     * --------------------------------------------------------------
+     */
+    public function testConstructWithoutValues()
+    {
+        $dot = new Dot;
+
+        $this->assertSame([], $dot->all());
+    }
+
+    public function testConstructWithArray()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertEquals('bar', $dot->get('foo'));
+    }
+
+    public function testConstructWithString()
+    {
+        $dot = new Dot('foobar');
+
+        $this->assertEquals('foobar', $dot->get(0));
+    }
+
+    public function testConstructWithDot()
+    {
+        $dot1 = new Dot(['foo' => 'bar']);
+        $dot2 = new Dot($dot1);
+
+        $this->assertEquals('bar', $dot2->get('foo'));
+    }
+
+    public function testConstructHelper()
+    {
+        $dot = dot(['foo' => 'bar']);
+
+        $this->assertInstanceOf(Dot::class, $dot);
+        $this->assertEquals('bar', $dot->get('foo'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Add
+     * --------------------------------------------------------------
+     */
+
+    public function testAddKeyValuePair()
+    {
+        $dot = new Dot;
+        $dot->add('foo.bar', 'baz');
+
+        $this->assertEquals('baz', $dot->get('foo.bar'));
+    }
+
+    public function testAddKeyValuePairWithCustomDelimeter()
+    {
+        $dot = new Dot([], '/');
+        $dot->add('foo/bar', 'baz');
+
+        $this->assertEquals('baz', $dot->get('foo/bar'));
+    }
+
+    public function testAddValueToExistingKey()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+        $dot->add('foo', 'baz');
+
+        $this->assertEquals('bar', $dot->get('foo'));
+    }
+
+    public function testAddArrayOfKeyValuePairs()
+    {
+        $dot = new Dot(['foobar' => 'baz']);
+        $dot->add([
+            'foobar' => 'qux',
+            'corge' => 'grault'
+        ]);
+
+        $this->assertSame(['foobar' => 'baz', 'corge' => 'grault'], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * All
+     * --------------------------------------------------------------
+     */
+
+    public function testAllReturnsAllItems()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertSame(['foo' => 'bar'], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Clear
+     * --------------------------------------------------------------
+     */
+
+    public function testClearKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->clear('foo.bar');
+
+        $this->assertSame([], $dot->get('foo.bar'));
+    }
+
+    public function testClearKeyWithCustomDelimiter()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']], '/');
+        $dot->clear('foo/bar');
+
+        $this->assertSame([], $dot->get('foo/bar'));
+    }
+
+    public function testClearNonExistingKey()
+    {
+        $dot = new Dot;
+        $dot->clear('foo');
+
+        $this->assertSame([], $dot->get('foo'));
+    }
+
+    public function testClearArrayOfKeys()
+    {
+        $dot = new Dot(['foo' => 'bar', 'baz' => 'qux']);
+        $dot->clear(['foo', 'baz']);
+
+        $this->assertSame(['foo' => [], 'baz' => []], $dot->all());
+    }
+
+    public function testClearAll()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+        $dot->clear();
+
+        $this->assertSame([], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Delete
+     * --------------------------------------------------------------
+     */
+
+    public function testDeleteKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->delete('foo.bar');
+
+        $this->assertFalse($dot->has('foo.bar'));
+    }
+
+    public function testDeleteKeyWithCustomDelimeter()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']], '/');
+        $dot->delete('foo/bar');
+
+        $this->assertFalse($dot->has('foo/bar'));
+    }
+
+    public function testDeleteNonExistingKey()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+        $dot->delete('baz.qux');
+
+        $this->assertSame(['foo' => 'bar'], $dot->all());
+    }
+
+    public function testDeleteArrayOfKeys()
+    {
+        $dot = new Dot(['foo' => 'bar', 'baz' => 'qux']);
+        $dot->delete(['foo', 'baz']);
+
+        $this->assertSame([], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Flatten
+     * --------------------------------------------------------------
+     */
+    public function testFlatten()
+    {
+        $dot = new Dot(['foo' => ['abc' => 'xyz', 'bar' => ['baz']]]);
+        $flatten = $dot->flatten();
+        $this->assertEquals('xyz', $flatten['foo.abc']);
+        $this->assertEquals('baz', $flatten['foo.bar.0']);
+    }
+
+    public function testFlattenWithCustomDelimiter()
+    {
+        $dot = new Dot(['foo' => ['abc' => 'xyz', 'bar' => ['baz']]], '/');
+        $flatten = $dot->flatten();
+        $this->assertEquals('xyz', $flatten['foo/abc']);
+        $this->assertEquals('baz', $flatten['foo/bar/0']);
+    }
+
+
+    public function testFlattenWithDoubleCustomDelimiter()
+    {
+        $dot = new Dot(['foo' => ['abc' => 'xyz', 'bar' => ['baz']]], '/');
+        $flatten = $dot->flatten('_');
+        $this->assertEquals('xyz', $flatten['foo_abc']);
+        $this->assertEquals('baz', $flatten['foo_bar_0']);
+    }
+
+
+    /*
+     * --------------------------------------------------------------
+     * Get
+     * --------------------------------------------------------------
+     */
+
+    public function testGetValueFromKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+
+        $this->assertEquals('baz', $dot->get('foo.bar'));
+    }
+
+    public function testGetValueFromNonExistingKey()
+    {
+        $dot = new Dot;
+
+        $this->assertNull($dot->get('foo'));
+    }
+
+    public function testGetGivenDefaultValueFromNonExistingKey()
+    {
+        $dot = new Dot;
+
+        $this->assertEquals('bar', $dot->get('foo', 'bar'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Has
+     * --------------------------------------------------------------
+     */
+
+    public function testHasKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+
+        $this->assertTrue($dot->has('foo.bar'));
+
+        $dot->delete('foo.bar');
+
+        $this->assertFalse($dot->has('foo.bar'));
+    }
+
+    public function testHasArrayOfKeys()
+    {
+        $dot = new Dot(['foo' => 'bar', 'baz' => 'qux']);
+
+        $this->assertTrue($dot->has(['foo', 'baz']));
+
+        $dot->delete('foo');
+
+        $this->assertFalse($dot->has(['foo', 'baz']));
+    }
+
+    public function testHasWithEmptyDot()
+    {
+        $dot = new Dot;
+
+        $this->assertFalse($dot->has('foo'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Is empty
+     * --------------------------------------------------------------
+     */
+
+    public function testIsEmptyDot()
+    {
+        $dot = new Dot;
+
+        $this->assertTrue($dot->isEmpty());
+
+        $dot->set('foo', 'bar');
+
+        $this->assertFalse($dot->isEmpty());
+    }
+
+    public function testIsEmptyKey()
+    {
+        $dot = new Dot;
+
+        $this->assertTrue($dot->isEmpty('foo.bar'));
+
+        $dot->set('foo.bar', 'baz');
+
+        $this->assertFalse($dot->isEmpty('foo.bar'));
+    }
+
+    public function testIsEmptyArrayOfKeys()
+    {
+        $dot = new Dot;
+
+        $this->assertTrue($dot->isEmpty(['foo', 'bar']));
+
+        $dot->set('foo', 'baz');
+
+        $this->assertFalse($dot->isEmpty(['foo', 'bar']));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Merge
+     * --------------------------------------------------------------
+     */
+
+    public function testMergeArrayWithDot()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->merge(['foo' => ['bar' => 'qux']]);
+
+        $this->assertEquals('qux', $dot->get('foo.bar'));
+    }
+
+    public function testMergeArrayWithKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->merge('foo', ['bar' => 'qux']);
+
+        $this->assertEquals('qux', $dot->get('foo.bar'));
+    }
+
+    public function testMergeDotWithDot()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['foo' => ['bar' => 'qux']]);
+        $dot1->merge($dot2);
+
+        $this->assertEquals('qux', $dot1->get('foo.bar'));
+    }
+
+    public function testMergeDotObjectWithKey()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['bar' => 'qux']);
+        $dot1->merge('foo', $dot2);
+
+        $this->assertEquals('qux', $dot1->get('foo.bar'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Recursive merge
+     * --------------------------------------------------------------
+     */
+
+    public function testRecursiveMergeArrayWithDot()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->mergeRecursive(['foo' => ['bar' => 'qux', 'quux' => 'quuz']]);
+
+        $this->assertEquals(['baz', 'qux'], $dot->get('foo.bar'));
+        $this->assertEquals('quuz', $dot->get('foo.quux'));
+    }
+
+    public function testRecursiveMergeArrayWithKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->mergeRecursive('foo', ['bar' => 'qux', 'quux' => 'quuz']);
+
+        $this->assertEquals(['baz', 'qux'], $dot->get('foo.bar'));
+        $this->assertEquals('quuz', $dot->get('foo.quux'));
+    }
+
+    public function testRecursiveMergeDotWithDot()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['foo' => ['bar' => 'qux', 'quux' => 'quuz']]);
+        $dot1->mergeRecursive($dot2);
+
+        $this->assertEquals(['baz', 'qux'], $dot1->get('foo.bar'));
+        $this->assertEquals('quuz', $dot1->get('foo.quux'));
+    }
+
+    public function testRecursiveMergeDotObjectWithKey()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['bar' => 'qux', 'quux' => 'quuz']);
+        $dot1->mergeRecursive('foo', $dot2);
+
+        $this->assertEquals(['baz', 'qux'], $dot1->get('foo.bar'));
+        $this->assertEquals('quuz', $dot1->get('foo.quux'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Recursive distinct merge
+     * --------------------------------------------------------------
+     */
+
+    public function testRecursiveDistinctMergeArrayWithDot()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->mergeRecursiveDistinct(['foo' => ['bar' => 'qux', 'quux' => 'quuz']]);
+
+        $this->assertEquals('qux', $dot->get('foo.bar'));
+        $this->assertEquals('quuz', $dot->get('foo.quux'));
+    }
+
+    public function testRecursiveDistinctMergeArrayWithKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->mergeRecursiveDistinct('foo', ['bar' => 'qux', 'quux' => 'quuz']);
+
+        $this->assertEquals('qux', $dot->get('foo.bar'));
+        $this->assertEquals('quuz', $dot->get('foo.quux'));
+    }
+
+    public function testRecursiveDistinctMergeDotWithDot()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['foo' => ['bar' => 'qux', 'quux' => 'quuz']]);
+        $dot1->mergeRecursiveDistinct($dot2);
+
+        $this->assertEquals('qux', $dot1->get('foo.bar'));
+        $this->assertEquals('quuz', $dot1->get('foo.quux'));
+    }
+
+    public function testRecursiveDistinctMergeDotObjectWithKey()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['bar' => 'qux', 'quux' => 'quuz']);
+        $dot1->mergeRecursiveDistinct('foo', $dot2);
+
+        $this->assertEquals('qux', $dot1->get('foo.bar'));
+        $this->assertEquals('quuz', $dot1->get('foo.quux'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Pull
+     * --------------------------------------------------------------
+     */
+
+    public function testPullKey()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertEquals('bar', $dot->pull('foo'));
+        $this->assertFalse($dot->has('foo'));
+    }
+
+    public function testPullNonExistingKey()
+    {
+        $dot = new Dot;
+
+        $this->assertNull($dot->pull('foo'));
+    }
+
+    public function testPullNonExistingKeyWithDefaultValue()
+    {
+        $dot = new Dot;
+
+        $this->assertEquals('bar', $dot->pull('foo', 'bar'));
+    }
+
+    public function testPullAll()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertSame(['foo' => 'bar'], $dot->pull());
+        $this->assertSame([], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Push
+     * --------------------------------------------------------------
+     */
+
+    public function testPushValue()
+    {
+        $dot = new Dot;
+        $dot->push('foo');
+
+        $this->assertEquals('foo', $dot->get(0));
+    }
+
+    public function testPushValueToKey()
+    {
+        $dot = new Dot(['foo' => [0 => 'bar']]);
+        $dot->push('foo', 'baz');
+
+        $this->assertSame(['bar', 'baz'], $dot->get('foo'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Replace
+     * --------------------------------------------------------------
+     */
+
+    public function testReplaceWithArray()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot->replace(['foo' => ['qux' => 'quux']]);
+
+        $this->assertEquals(['qux' => 'quux'], $dot->get('foo'));
+    }
+
+    public function testReplaceKeyWithArray()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'baz', 'qux' => 'quux']]);
+        $dot->replace('foo', ['qux' => 'corge']);
+
+        $this->assertEquals(['bar' => 'baz', 'qux' => 'corge'], $dot->get('foo'));
+    }
+
+    public function testReplaceWithDot()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz']]);
+        $dot2 = new Dot(['foo' => ['bar' => 'qux']]);
+        $dot1->replace($dot2);
+
+        $this->assertEquals(['bar' => 'qux'], $dot1->get('foo'));
+    }
+
+    public function testReplaceKeyWithDot()
+    {
+        $dot1 = new Dot(['foo' => ['bar' => 'baz', 'qux' => 'quux']]);
+        $dot2 = new Dot(['qux' => 'corge']);
+        $dot1->merge('foo', $dot2);
+
+        $this->assertEquals(['bar' => 'baz', 'qux' => 'corge'], $dot1->get('foo'));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Set
+     * --------------------------------------------------------------
+     */
+
+    public function testSetKeyValuePair()
+    {
+        $dot = new Dot;
+        $dot->set('foo.bar', 'baz');
+
+        $this->assertEquals('baz', $dot->get('foo.bar'));
+    }
+
+    public function testSetKeyValuePairWithCustomDelimiter()
+    {
+        $dot = new Dot([], '/');
+        $dot->set('foo/bar', 'baz');
+
+        $this->assertEquals('baz', $dot->get('foo/bar'));
+    }
+
+
+    public function testSetArrayOfKeyValuePairs()
+    {
+        $dot = new Dot;
+        $dot->set(['foo' => 'bar', 'baz' => 'qux']);
+
+        $this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Set array
+     * --------------------------------------------------------------
+     */
+
+    public function testSetArray()
+    {
+        $dot = new Dot;
+        $dot->setArray(['foo' => 'bar']);
+
+        $this->assertSame(['foo' => 'bar'], $dot->all());
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Set reference
+     * --------------------------------------------------------------
+     */
+
+    public function testSetReference()
+    {
+        $dot = new Dot;
+        $items = ['foo' => 'bar'];
+        $dot->setReference($items);
+        $dot->set('foo', 'baz');
+
+        $this->assertEquals('baz', $items['foo']);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * ArrayAccess interface
+     * --------------------------------------------------------------
+     */
+
+    public function testOffsetExists()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertTrue(isset($dot['foo']));
+
+        unset($dot['foo']);
+
+        $this->assertFalse(isset($dot['foo']));
+    }
+
+    public function testOffsetGet()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertEquals('bar', $dot['foo']);
+    }
+
+    public function testOffsetSet()
+    {
+        $dot = new Dot;
+        $dot['foo.bar'] = 'baz';
+
+        $this->assertEquals('baz', $dot['foo.bar']);
+    }
+
+    public function testOffsetSetWithoutKey()
+    {
+        $dot = new Dot;
+        $dot[] = 'foobar';
+
+        $this->assertEquals('foobar', $dot->get(0));
+    }
+
+    public function testOffsetUnset()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+        unset($dot['foo']);
+
+        $this->assertFalse(isset($dot['foo']));
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * To JSON
+     * --------------------------------------------------------------
+     */
+
+    public function testToJsonAll()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertJsonStringEqualsJsonString(
+            json_encode(['foo' => 'bar']),
+            $dot->toJson()
+        );
+    }
+
+    public function testToJsonAllWithOption()
+    {
+        $dot = new Dot(['foo' => "'bar'"]);
+
+        $this->assertJsonStringEqualsJsonString(
+            json_encode(['foo' => "'bar'"], JSON_HEX_APOS),
+            $dot->toJson(JSON_HEX_APOS)
+        );
+    }
+
+    public function testToJsonKey()
+    {
+        $dot = new Dot(['foo' => ['bar' => 'value']]);
+
+        $this->assertJsonStringEqualsJsonString(
+            json_encode(['bar' => "value"]),
+            $dot->toJson('foo')
+        );
+    }
+
+    public function testToJsonKeyWithOptions()
+    {
+        $dot = new Dot(['foo' => ['bar' => "'value'"]]);
+
+        $this->assertEquals(
+            json_encode(['bar' => "'value'"], JSON_HEX_APOS),
+            $dot->toJson('foo', JSON_HEX_APOS)
+        );
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * Countable interface
+     * --------------------------------------------------------------
+     */
+
+    public function testCount()
+    {
+        $dot = new Dot([1, 2, 3]);
+
+        $this->assertEquals(3, $dot->count());
+    }
+
+    public function testCountable()
+    {
+        $dot = new Dot([1, 2, 3]);
+
+        $this->assertCount(3, $dot);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * IteratorAggregate interface
+     * --------------------------------------------------------------
+     */
+
+    public function testGetIteratorReturnsArrayIterator()
+    {
+        $dot = new Dot;
+
+        $this->assertInstanceOf(ArrayIterator::class, $dot->getIterator());
+    }
+
+    public function testIterationReturnsOriginalValues()
+    {
+        $dot = new Dot([1, 2, 3]);
+
+        foreach ($dot as $item) {
+            $items[] = $item;
+        }
+
+        $this->assertSame([1, 2, 3], $items);
+    }
+
+    /*
+     * --------------------------------------------------------------
+     * JsonSerializable interface
+     * --------------------------------------------------------------
+     */
+
+    public function testJsonEncodingReturnsJson()
+    {
+        $dot = new Dot(['foo' => 'bar']);
+
+        $this->assertJsonStringEqualsJsonString(
+            json_encode(['foo' => 'bar']),
+            json_encode($dot)
+        );
+    }
+}

+ 8 - 0
vendor/asm89/stack-cors/.gitattributes

@@ -0,0 +1,8 @@
+* text=auto
+
+/tests export-ignore
+.github export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+phpunit.xml.dist export-ignore
+CHANGELOG.md export-ignore

+ 62 - 0
vendor/asm89/stack-cors/.github/workflows/run-tests.yml

@@ -0,0 +1,62 @@
+on:
+  push:
+  pull_request:
+  schedule:
+  - cron: '0 0 * * *'
+
+jobs:
+  php-tests:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        php: [8.1, 8.2, 8.3]
+        symfony: [6.x]
+        dependency-version: [prefer-lowest, prefer-stable]
+        os: [ubuntu-latest]
+        include:
+          - symfony: ^5.3
+            php: 7.3
+            dependency-version: prefer-lowest
+          - symfony: ^5.3
+            php: 7.3
+            dependency-version: prefer-stable
+          - symfony: ^5.3
+            php: 7.4
+            dependency-version: prefer-stable
+          - symfony: ^5.3
+            php: 8.1
+            dependency-version: prefer-stable
+          - symfony: ^7
+            php: 8.2
+            dependency-version: prefer-lowest
+          - symfony: ^7
+            php: 8.2
+            dependency-version: prefer-stable
+          - symfony: ^7
+            php: 8.3
+            dependency-version: prefer-stable
+
+    name: PHP${{ matrix.php }} Symfony${{ matrix.symfony }} - ${{ matrix.dependency-version }}
+
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v1
+
+    - name: Setup PHP
+      uses: shivammathur/setup-php@v2
+      with:
+        php-version: ${{ matrix.php }}
+        extensions: mbstring
+
+    - name: Install dependencies
+      run: |
+        composer require "symfony/http-foundation:${{ matrix.symfony }}" "symfony/http-kernel:${{ matrix.symfony }}" --no-interaction --no-update
+        composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest --with-all-dependencies
+
+    - name: Execute Unit Tests
+      run: vendor/bin/phpunit
+
+    - name: Check PSR-12 Codestyle
+      run: vendor/bin/phpcs --standard=psr12 --exclude=Generic.Files.LineLength src/
+      if: matrix.os == 'ubuntu-latest'

+ 5 - 0
vendor/asm89/stack-cors/.gitignore

@@ -0,0 +1,5 @@
+phpunit.xml
+composer.lock
+composer.phar
+/vendor/
+.phpunit.result.cache

+ 21 - 0
vendor/asm89/stack-cors/CHANGELOG.md

@@ -0,0 +1,21 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Fixed
+
+- Bug with `Access-Control-Allow-Origin` header is `null` then `allowedOrigins` is `['*']`, `supportsCredentials` is `true` and `Origin` header doesn't  set (#85)
+
+## [2.0.0] - 2020-05-11
+
+### Added
+- CORS headers are better cachable now, with correct Vary headers (#70, #74)
+
+### Changed
+- CORS headers are added to non-Origin requests when possible (#73)
+- Requests are no longer blocked by the server, only by the browser (#70)

+ 19 - 0
vendor/asm89/stack-cors/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2013-2017 Alexander <iam.asm89@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 85 - 0
vendor/asm89/stack-cors/README.md

@@ -0,0 +1,85 @@
+# Stack/Cors
+
+Library and middleware enabling cross-origin resource sharing for your
+http-{foundation,kernel} using application. It attempts to implement the
+[W3C Recommendation] for cross-origin resource sharing.
+
+[W3C Recommendation]: http://www.w3.org/TR/cors/
+
+Build status: ![.github/workflows/run-tests.yml](https://github.com/asm89/stack-cors/workflows/.github/workflows/run-tests.yml/badge.svg)
+
+## Installation
+
+Require `asm89/stack-cors` using composer.
+
+## Usage
+
+This package can be used as a library or as [stack middleware].
+
+[stack middleware]: http://stackphp.com/
+
+### Options
+
+| Option                 | Description                                                | Default value |
+|------------------------|------------------------------------------------------------|---------------|
+| `allowedMethods`         | Matches the request method.                                | `[]`          |
+| `allowedOrigins`         | Matches the request origin.                                | `[]`          |
+| `allowedOriginsPatterns` | Matches the request origin with `preg_match`.              | `[]`          |
+| `allowedHeaders`         | Sets the Access-Control-Allow-Headers response header.     | `[]`          |
+| `exposedHeaders`         | Sets the Access-Control-Expose-Headers response header.    | `false`       |
+| `maxAge`                 | Sets the Access-Control-Max-Age response header.<br/>Set to `null` to omit the header/use browser default.           | `0`       |
+| `supportsCredentials`    | Sets the Access-Control-Allow-Credentials header.          | `false`       |
+
+The _allowedMethods_ and _allowedHeaders_ options are case-insensitive.
+
+You don't need to provide both _allowedOrigins_ and _allowedOriginsPatterns_. If one of the strings passed matches, it is considered a valid origin.
+
+If `['*']` is provided to _allowedMethods_, _allowedOrigins_ or _allowedHeaders_ all methods / origins / headers are allowed.
+
+If _supportsCredentials_ is `true`, you must [explicitly set](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials) `allowedHeaders` for any headers which are not CORS safelisted.
+
+### Example: using the library
+
+```php
+<?php
+
+use Asm89\Stack\CorsService;
+
+$cors = new CorsService([
+    'allowedHeaders'         => ['x-allowed-header', 'x-other-allowed-header'],
+    'allowedMethods'         => ['DELETE', 'GET', 'POST', 'PUT'],
+    'allowedOrigins'         => ['http://localhost'],
+    'allowedOriginsPatterns' => ['/localhost:\d/'],
+    'exposedHeaders'         => false,
+    'maxAge'                 => 600,
+    'supportsCredentials'    => true,
+]);
+
+$cors->addActualRequestHeaders(Response $response, $origin);
+$cors->handlePreflightRequest(Request $request);
+$cors->isActualRequestAllowed(Request $request);
+$cors->isCorsRequest(Request $request);
+$cors->isPreflightRequest(Request $request);
+```
+
+## Example: using the stack middleware
+
+```php
+<?php
+
+use Asm89\Stack\Cors;
+
+$app = new Cors($app, [
+    // you can use ['*'] to allow any headers
+    'allowedHeaders'      => ['x-allowed-header', 'x-other-allowed-header'],
+    // you can use ['*'] to allow any methods
+    'allowedMethods'      => ['DELETE', 'GET', 'POST', 'PUT'],
+    // you can use ['*'] to allow requests from any origin
+    'allowedOrigins'      => ['localhost'],
+    // you can enter regexes that are matched to the origin request header
+    'allowedOriginsPatterns' => ['/localhost:\d/'],
+    'exposedHeaders'      => false,
+    'maxAge'              => 600,
+    'supportsCredentials' => false,
+]);
+```

+ 45 - 0
vendor/asm89/stack-cors/composer.json

@@ -0,0 +1,45 @@
+{
+    "name": "asm89/stack-cors",
+    "description": "Cross-origin resource sharing library and stack middleware",
+    "keywords": ["stack", "cors"],
+    "homepage": "https://github.com/asm89/stack-cors",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Alexander",
+            "email": "iam.asm89@gmail.com"
+        }
+    ],
+    "require": {
+        "php": "^7.3|^8.0",
+        "symfony/http-foundation": "^5.3|^6|^7",
+        "symfony/http-kernel": "^5.3|^6|^7"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9",
+        "squizlabs/php_codesniffer": "^3.5"
+    },
+    "autoload": {
+        "psr-4": {
+            "Asm89\\Stack\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Asm89\\Stack\\Tests\\": "tests/"
+        }
+    },
+    "scripts": {
+        "test": "phpunit",
+        "check-style": "phpcs -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src",
+        "fix-style": "phpcbf -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src"
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "2.2-dev"
+        }
+    },
+    "minimum-stability": "beta",
+    "prefer-stable": true
+}

+ 24 - 0
vendor/asm89/stack-cors/phpunit.xml.dist

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit backupGlobals="false"
+         backupStaticAttributes="false"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false"
+>
+  <testsuites>
+    <testsuite name="Stack Cors Test Suite">
+      <directory>./tests</directory>
+    </testsuite>
+  </testsuites>
+
+  <filter>
+    <whitelist>
+      <directory suffix=".php">./src/</directory>
+    </whitelist>
+  </filter>
+
+</phpunit>

+ 61 - 0
vendor/asm89/stack-cors/src/Cors.php

@@ -0,0 +1,61 @@
+<?php
+
+/*
+ * This file is part of asm89/stack-cors.
+ *
+ * (c) Alexander <iam.asm89@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Asm89\Stack;
+
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+class Cors implements HttpKernelInterface
+{
+    /**
+     * @var \Symfony\Component\HttpKernel\HttpKernelInterface
+     */
+    private $app;
+
+    /**
+     * @var \Asm89\Stack\CorsService
+     */
+    private $cors;
+
+    private $defaultOptions = [
+        'allowedHeaders'         => [],
+        'allowedMethods'         => [],
+        'allowedOrigins'         => [],
+        'allowedOriginsPatterns' => [],
+        'exposedHeaders'         => [],
+        'maxAge'                 => 0,
+        'supportsCredentials'    => false,
+    ];
+
+    public function __construct(HttpKernelInterface $app, array $options = [])
+    {
+        $this->app  = $app;
+        $this->cors = new CorsService(array_merge($this->defaultOptions, $options));
+    }
+
+    public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
+    {
+        if ($this->cors->isPreflightRequest($request)) {
+            $response = $this->cors->handlePreflightRequest($request);
+            return $this->cors->varyHeader($response, 'Access-Control-Request-Method');
+        }
+
+        $response = $this->app->handle($request, $type, $catch);
+
+        if ($request->getMethod() === 'OPTIONS') {
+            $this->cors->varyHeader($response, 'Access-Control-Request-Method');
+        }
+
+        return $this->cors->addActualRequestHeaders($response, $request);
+    }
+}

+ 225 - 0
vendor/asm89/stack-cors/src/CorsService.php

@@ -0,0 +1,225 @@
+<?php
+
+/*
+ * This file is part of asm89/stack-cors.
+ *
+ * (c) Alexander <iam.asm89@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Asm89\Stack;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class CorsService
+{
+    private $options;
+
+    public function __construct(array $options = [])
+    {
+        $this->options = $this->normalizeOptions($options);
+    }
+
+    private function normalizeOptions(array $options = []): array
+    {
+        $options += [
+            'allowedOrigins' => [],
+            'allowedOriginsPatterns' => [],
+            'supportsCredentials' => false,
+            'allowedHeaders' => [],
+            'exposedHeaders' => [],
+            'allowedMethods' => [],
+            'maxAge' => 0,
+        ];
+
+        // normalize array('*') to true
+        if (in_array('*', $options['allowedOrigins'])) {
+            $options['allowedOrigins'] = true;
+        }
+        if (in_array('*', $options['allowedHeaders'])) {
+            $options['allowedHeaders'] = true;
+        } else {
+            $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
+        }
+
+        if (in_array('*', $options['allowedMethods'])) {
+            $options['allowedMethods'] = true;
+        } else {
+            $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
+        }
+
+        return $options;
+    }
+
+    /**
+     * @deprecated use isOriginAllowed
+     */
+    public function isActualRequestAllowed(Request $request): bool
+    {
+        return $this->isOriginAllowed($request);
+    }
+
+    public function isCorsRequest(Request $request): bool
+    {
+        return $request->headers->has('Origin');
+    }
+
+    public function isPreflightRequest(Request $request): bool
+    {
+        return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method');
+    }
+
+    public function handlePreflightRequest(Request $request): Response
+    {
+        $response = new Response();
+
+        $response->setStatusCode(204);
+
+        return $this->addPreflightRequestHeaders($response, $request);
+    }
+
+    public function addPreflightRequestHeaders(Response $response, Request $request): Response
+    {
+        $this->configureAllowedOrigin($response, $request);
+
+        if ($response->headers->has('Access-Control-Allow-Origin')) {
+            $this->configureAllowCredentials($response, $request);
+
+            $this->configureAllowedMethods($response, $request);
+
+            $this->configureAllowedHeaders($response, $request);
+
+            $this->configureMaxAge($response, $request);
+        }
+
+        return $response;
+    }
+
+    public function isOriginAllowed(Request $request): bool
+    {
+        if ($this->options['allowedOrigins'] === true) {
+            return true;
+        }
+
+        if (!$request->headers->has('Origin')) {
+            return false;
+        }
+
+        $origin = $request->headers->get('Origin');
+
+        if (in_array($origin, $this->options['allowedOrigins'])) {
+            return true;
+        }
+
+        foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
+            if (preg_match($pattern, $origin)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public function addActualRequestHeaders(Response $response, Request $request): Response
+    {
+        $this->configureAllowedOrigin($response, $request);
+
+        if ($response->headers->has('Access-Control-Allow-Origin')) {
+            $this->configureAllowCredentials($response, $request);
+
+            $this->configureExposedHeaders($response, $request);
+        }
+
+        return $response;
+    }
+
+    private function configureAllowedOrigin(Response $response, Request $request)
+    {
+        if ($this->options['allowedOrigins'] === true && !$this->options['supportsCredentials']) {
+            // Safe+cacheable, allow everything
+            $response->headers->set('Access-Control-Allow-Origin', '*');
+        } elseif ($this->isSingleOriginAllowed()) {
+            // Single origins can be safely set
+            $response->headers->set('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]);
+        } else {
+            // For dynamic headers, set the requested Origin header when set and allowed
+            if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
+                $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
+            }
+
+            $this->varyHeader($response, 'Origin');
+        }
+    }
+
+    private function isSingleOriginAllowed(): bool
+    {
+        if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) {
+            return false;
+        }
+
+        return count($this->options['allowedOrigins']) === 1;
+    }
+
+    private function configureAllowedMethods(Response $response, Request $request)
+    {
+        if ($this->options['allowedMethods'] === true) {
+            $allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method'));
+            $this->varyHeader($response, 'Access-Control-Request-Method');
+        } else {
+            $allowMethods = implode(', ', $this->options['allowedMethods']);
+        }
+
+        $response->headers->set('Access-Control-Allow-Methods', $allowMethods);
+    }
+
+    private function configureAllowedHeaders(Response $response, Request $request)
+    {
+        if ($this->options['allowedHeaders'] === true) {
+            $allowHeaders = $request->headers->get('Access-Control-Request-Headers');
+            $this->varyHeader($response, 'Access-Control-Request-Headers');
+        } else {
+            $allowHeaders = implode(', ', $this->options['allowedHeaders']);
+        }
+        $response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
+    }
+
+    private function configureAllowCredentials(Response $response, Request $request)
+    {
+        if ($this->options['supportsCredentials']) {
+            $response->headers->set('Access-Control-Allow-Credentials', 'true');
+        }
+    }
+
+    private function configureExposedHeaders(Response $response, Request $request)
+    {
+        if ($this->options['exposedHeaders']) {
+            $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
+        }
+    }
+
+    private function configureMaxAge(Response $response, Request $request)
+    {
+        if ($this->options['maxAge'] !== null) {
+            $response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']);
+        }
+    }
+
+    public function varyHeader(Response $response, $header): Response
+    {
+        if (!$response->headers->has('Vary')) {
+            $response->headers->set('Vary', $header);
+        } elseif (!in_array($header, explode(', ', $response->headers->get('Vary')))) {
+            $response->headers->set('Vary', $response->headers->get('Vary') . ', ' . $header);
+        }
+
+        return $response;
+    }
+
+    private function isSameHost(Request $request): bool
+    {
+        return $request->headers->get('Origin') === $request->getSchemeAndHttpHost();
+    }
+}

+ 50 - 0
vendor/asm89/stack-cors/tests/CorsServiceTest.php

@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * This file is part of asm89/stack-cors.
+ *
+ * (c) Alexander <iam.asm89@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Asm89\Stack\Tests;
+
+use Asm89\Stack\CorsService;
+use PHPUnit\Framework\TestCase;
+
+class CorsServiceTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function it_can_have_options()
+    {
+        $service = new CorsService([
+            'allowedOrigins' => ['*']
+        ]);
+
+        $this->assertInstanceOf(CorsService::class, $service);
+    }
+
+    /**
+     * @test
+     */
+    public function it_can_have_no_options()
+    {
+        $service = new CorsService();
+        $this->assertInstanceOf(CorsService::class, $service);
+
+    }
+
+    /**
+     * @test
+     */
+    public function it_can_have_empty_options()
+    {
+        $service = new CorsService([]);
+        $this->assertInstanceOf(CorsService::class, $service);
+
+    }
+}

+ 569 - 0
vendor/asm89/stack-cors/tests/CorsTest.php

@@ -0,0 +1,569 @@
+<?php
+
+/*
+ * This file is part of asm89/stack-cors.
+ *
+ * (c) Alexander <iam.asm89@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Asm89\Stack\Tests;
+
+use Asm89\Stack\Cors;
+use Asm89\Stack\CorsService;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class CorsTest extends TestCase
+{
+    /**
+     * @test
+     */
+    public function it_does_modify_on_a_request_without_origin()
+    {
+        $app                = $this->createStackedApp();
+
+        $response = $app->handle(new Request());
+
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_does_modify_on_a_request_with_same_origin()
+    {
+        $app = $this->createStackedApp(array('allowedOrigins' => array('*')));
+        $unmodifiedResponse = new Response();
+
+        $request  = new Request();
+        $request->headers->set('Host', 'foo.com');
+        $request->headers->set('Origin', 'http://foo.com');
+        $response = $app->handle($request);
+
+        $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_allow_origin_header_on_valid_actual_request()
+    {
+        $app      = $this->createStackedApp();
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Origin'));
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_allow_origin_header_on_allow_all_origin_request()
+    {
+        $app      = $this->createStackedApp(array('allowedOrigins' => array('*')));
+        $request  = new Request();
+        $request->headers->set('Origin', 'http://localhost');
+
+        $response = $app->handle($request);
+
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Origin'));
+        $this->assertEquals('*', $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_allow_headers_header_on_allow_all_headers_request()
+    {
+        $app     = $this->createStackedApp(array('allowedHeaders' => array('*')));
+        $request = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Headers', 'Foo, BAR');
+
+        $response = $app->handle($request);
+
+        $this->assertEquals(204, $response->getStatusCode());
+        $this->assertEquals('Foo, BAR', $response->headers->get('Access-Control-Allow-Headers'));
+        $this->assertEquals('Access-Control-Request-Headers, Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_allow_headers_header_on_allow_all_headers_request_credentials()
+    {
+        $app      = $this->createStackedApp(array('allowedHeaders' => array('*'), 'supportsCredentials' => true));
+        $request = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Headers', 'Foo, BAR');
+
+        $response = $app->handle($request);
+
+        $this->assertEquals(204, $response->getStatusCode());
+        $this->assertEquals('Foo, BAR', $response->headers->get('Access-Control-Allow-Headers'));
+        $this->assertEquals('Access-Control-Request-Headers, Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_sets_allow_credentials_header_when_flag_is_set_on_valid_actual_request()
+    {
+        $app     = $this->createStackedApp(array('supportsCredentials' => true));
+        $request = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Credentials'));
+        $this->assertEquals('true', $response->headers->get('Access-Control-Allow-Credentials'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_does_not_set_allow_credentials_header_when_flag_is_not_set_on_valid_actual_request()
+    {
+        $app     = $this->createStackedApp();
+        $request = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertFalse($response->headers->has('Access-Control-Allow-Credentials'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_sets_exposed_headers_when_configured_on_actual_request()
+    {
+        $app     = $this->createStackedApp(array('exposedHeaders' => array('x-exposed-header', 'x-another-exposed-header')));
+        $request = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Expose-Headers'));
+        $this->assertEquals('x-exposed-header, x-another-exposed-header', $response->headers->get('Access-Control-Expose-Headers'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_adds_a_vary_header_when_wildcard_and_supports_credentials()
+    {
+        $app      = $this->createStackedApp(array(
+            'allowedOrigins' => ['*'],
+            'supportsCredentials' => true,
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Vary'));
+        $this->assertEquals('Origin', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_adds_multiple_vary_header_when_wildcard_and_supports_credentials()
+    {
+        $app = $this->createStackedApp(array(
+            'allowedOrigins' => ['*'],
+            'allowedMethods' => ['*'],
+            'supportsCredentials' => true,
+        ));
+        $request  = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Vary'));
+        $this->assertEquals('Origin, Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_adds_a_vary_header_when_has_origin_patterns()
+    {
+        $app      = $this->createStackedApp(array(
+            'allowedOriginsPatterns' => array('/l(o|0)calh(o|0)st/')
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Vary'));
+        $this->assertEquals('Origin', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_doesnt_add_a_vary_header_when_wilcard_origins()
+    {
+        $app      = $this->createStackedApp(array(
+            'allowedOrigins' => array('*', 'http://localhost')
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertFalse($response->headers->has('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_doesnt_add_a_vary_header_when_simple_origins()
+    {
+        $app = $this->createStackedApp(array(
+            'allowedOrigins' => array('http://localhost')
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+        $this->assertFalse($response->headers->has('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_adds_a_vary_header_when_multiple_origins()
+    {
+        $app = $this->createStackedApp(array(
+           'allowedOrigins' => array('http://localhost', 'http://example.com')
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+        $this->assertTrue($response->headers->has('Vary'));
+    }
+
+    /**
+     * @test
+     * @see http://www.w3.org/TR/cors/index.html#resource-implementation
+     */
+    public function it_appends_an_existing_vary_header()
+    {
+        $app      = $this->createStackedApp(
+            array(
+                'allowedOrigins' => ['*'],
+                'supportsCredentials' => true,
+            ),
+            array(
+                'Vary' => 'Content-Type'
+            )
+        );
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Vary'));
+        $this->assertEquals('Content-Type, Origin', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_access_control_headers_on_cors_request()
+    {
+        $app      = $this->createStackedApp();
+        $request  = new Request();
+        $request->headers->set('Origin', 'http://localhost');
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Origin'));
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_access_control_headers_on_cors_request_with_pattern_origin()
+    {
+        $app = $this->createStackedApp(array(
+          'allowedOrigins' => array(),
+          'allowedOriginsPatterns' => array('/l(o|0)calh(o|0)st/')
+        ));
+        $request  = $this->createValidActualRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Origin'));
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+        $this->assertTrue($response->headers->has('Vary'));
+        $this->assertEquals('Origin', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_adds_vary_headers_on_preflight_non_preflight_options()
+    {
+        $app      = $this->createStackedApp();
+        $request  = new Request();
+        $request->setMethod('OPTIONS');
+
+        $response = $app->handle($request);
+
+        $this->assertEquals('Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_access_control_headers_on_valid_preflight_request()
+    {
+        $app     = $this->createStackedApp();
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Origin'));
+        $this->assertEquals('http://localhost', $response->headers->get('Access-Control-Allow-Origin'));
+        $this->assertEquals('Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_does_not_allow_request_with_origin_not_allowed()
+    {
+        $passedOptions = array(
+          'allowedOrigins' => array('http://notlocalhost'),
+        );
+
+        $service  = new CorsService($passedOptions);
+        $request  = $this->createValidActualRequest();
+        $response = new Response();
+        $service->addActualRequestHeaders($response, $request);
+
+        $this->assertNotEquals($request->headers->get('Origin'), $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_does_not_modify_request_with_pattern_origin_not_allowed()
+    {
+        $passedOptions = array(
+            'allowedOrigins' => array(),
+            'allowedOriginsPatterns' => array('/l\dcalh\dst/')
+        );
+
+        $service  = new CorsService($passedOptions);
+        $request  = $this->createValidActualRequest();
+        $response = new Response();
+        $service->addActualRequestHeaders($response, $request);
+
+        $this->assertNotEquals($request->headers->get('Origin'), $response->headers->get('Access-Control-Allow-Origin'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_allow_methods_on_valid_preflight_request()
+    {
+        $app     = $this->createStackedApp(array('allowedMethods' => array('get', 'put')));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Methods'));
+        // it will uppercase the methods
+        $this->assertEquals('GET, PUT', $response->headers->get('Access-Control-Allow-Methods'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_valid_preflight_request_with_allow_methods_all()
+    {
+        $app     = $this->createStackedApp(array('allowedMethods' => array('*')));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Methods'));
+        // it will return the Access-Control-Request-Method pass in the request
+        $this->assertEquals('GET', $response->headers->get('Access-Control-Allow-Methods'));
+        $this->assertEquals('Access-Control-Request-Method', $response->headers->get('Vary'));
+
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_valid_preflight_request_with_allow_methods_all_credentials()
+    {
+        $app     = $this->createStackedApp(array('allowedMethods' => array('*'), 'supportsCredentials' => true));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Methods'));
+        // it will return the Access-Control-Request-Method pass in the request
+        $this->assertEquals('GET', $response->headers->get('Access-Control-Allow-Methods'));
+        // it should vary this header
+        $this->assertEquals('Access-Control-Request-Method', $response->headers->get('Vary'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_returns_ok_on_valid_preflight_request_with_requested_headers_allowed()
+    {
+        $app            = $this->createStackedApp();
+        $requestHeaders = 'X-Allowed-Header, x-other-allowed-header';
+        $request        = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Headers', $requestHeaders);
+
+        $response = $app->handle($request);
+
+        $this->assertEquals(204, $response->getStatusCode());
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Headers'));
+        // the response will have the "allowedHeaders" value passed to Cors rather than the request one
+        $this->assertEquals('x-allowed-header, x-other-allowed-header', $response->headers->get('Access-Control-Allow-Headers'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_sets_allow_credentials_header_when_flag_is_set_on_valid_preflight_request()
+    {
+        $app     = $this->createStackedApp(array('supportsCredentials' => true));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Allow-Credentials'));
+        $this->assertEquals('true', $response->headers->get('Access-Control-Allow-Credentials'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_does_not_set_allow_credentials_header_when_flag_is_not_set_on_valid_preflight_request()
+    {
+        $app     = $this->createStackedApp();
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertFalse($response->headers->has('Access-Control-Allow-Credentials'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_sets_max_age_when_set()
+    {
+        $app     = $this->createStackedApp(array('maxAge' => 42));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Max-Age'));
+        $this->assertEquals(42, $response->headers->get('Access-Control-Max-Age'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_sets_max_age_when_zero()
+    {
+        $app     = $this->createStackedApp(array('maxAge' => 0));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertTrue($response->headers->has('Access-Control-Max-Age'));
+        $this->assertEquals(0, $response->headers->get('Access-Control-Max-Age'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_doesnt_set_max_age_when_false()
+    {
+        $app     = $this->createStackedApp(array('maxAge' => null));
+        $request = $this->createValidPreflightRequest();
+
+        $response = $app->handle($request);
+
+        $this->assertFalse($response->headers->has('Access-Control-Max-Age'));
+    }
+
+    /**
+     * @test
+     */
+    public function it_skips_empty_access_control_request_header()
+    {
+        $app     = $this->createStackedApp();
+        $request = $this->createValidPreflightRequest();
+        $request->headers->set('Access-Control-Request-Headers', '');
+
+        $response = $app->handle($request);
+        $this->assertEquals(204, $response->getStatusCode());
+    }
+
+    /**
+     * @test
+     */
+    public function it_doesnt_set_access_control_allow_origin_without_origin()
+    {
+        $app     = $this->createStackedApp([
+            'allowedOrigins'      => ['*'],
+            'supportsCredentials' => true,
+        ]);
+
+        $response = $app->handle(new Request);
+
+        $this->assertFalse($response->headers->has('Access-Control-Allow-Origin'));
+    }
+
+    private function createValidActualRequest()
+    {
+        $request  = new Request();
+        $request->headers->set('Origin', 'http://localhost');
+
+        return $request;
+    }
+
+    private function createValidPreflightRequest()
+    {
+        $request  = new Request();
+        $request->headers->set('Origin', 'http://localhost');
+        $request->headers->set('Access-Control-Request-Method', 'get');
+        $request->setMethod('OPTIONS');
+
+        return $request;
+    }
+
+    private function createStackedApp(array $options = array(), array $responseHeaders = array())
+    {
+        $passedOptions = array_merge(array(
+                'allowedHeaders'      => array('x-allowed-header', 'x-other-allowed-header'),
+                'allowedMethods'      => array('delete', 'get', 'post', 'put'),
+                'allowedOrigins'      => array('http://localhost'),
+                'exposedHeaders'      => false,
+                'maxAge'              => false,
+                'supportsCredentials' => false,
+            ),
+            $options
+        );
+
+        return new Cors(new MockApp($responseHeaders), $passedOptions);
+    }
+}

+ 27 - 0
vendor/asm89/stack-cors/tests/MockApp.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace Asm89\Stack\Tests;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+class MockApp implements HttpKernelInterface
+{
+
+    private $responseHeaders;
+
+    public function __construct(array $responseHeaders)
+    {
+        $this->responseHeaders = $responseHeaders;
+    }
+
+    public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
+    {
+        $response = new Response();
+
+        $response->headers->add($this->responseHeaders);
+
+        return $response;
+    }
+}