Skip to content

Instantly share code, notes, and snippets.

@santaklouse
Created September 15, 2021 19:32
Show Gist options
  • Select an option

  • Save santaklouse/77b10ae01906fdaf96ea74fea6b42a63 to your computer and use it in GitHub Desktop.

Select an option

Save santaklouse/77b10ae01906fdaf96ea74fea6b42a63 to your computer and use it in GitHub Desktop.
Simple compression of json API response for js table (Experiment)
transformResponse: (data, headersGetter, status) => {
if (status >= 300) {
return {};
}
// decompressing
return MappedJSON.decompress(angular.fromJson(data));
}
.factory('MappedJSON',[function() {
this.map = [];
this.extracted = [];
const restoreExtractedData = item => {
const extractKeys = _.keys(this.extracted);
if (!extractKeys.length) {
return;
}
const extracted = this.extracted;
for (const eKey of extractKeys) {
const indexStr = extracted[eKey].index;
const eList = extracted[eKey].list;
const subitems = item[eKey];
if (!subitems) {
item[eKey] = [];
continue;
}
for (const sKey in subitems) {
const sItem = subitems[sKey];
const eItemKey = sItem[indexStr];
const eItem = eList[eItemKey];
Object.assign(item[eKey][sKey], eItem);
}
}
};
const restoreMappedFieldNames = (key, list) => {
const map = this.map;
for (const subKey in list[key]) {
if (!_.isArray(list[key][subKey])) {
continue;
}
for (const subItemKey in list[key][subKey]) {
list[key][subKey][subItemKey] = _.mapKeys(
list[key][subKey][subItemKey],
(value, key) => map[key] || key
);
}
}
list[key] = _.mapKeys(list[key], (value, key) => map[key] || key);
};
const decompress = (data = []) => {
const { map, list, extracted } = data;
if (!map) {
return data;
}
this.map = map;
this.extracted = extracted;
for (const key in list) {
restoreExtractedData(list[key]);
restoreMappedFieldNames(key, list);
}
// before exit
this.map = [];
this.extracted = [];
delete data.map;
delete data.extracted;
return data;
};
return {
decompress
}
}])
<?php
namespace RedactedBundle\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class RedactedController extends Controller
{
const LOCATIONS_LITERAL = 'redacted';
public function redactedAction(Request $request)
{
$list = $redactedRepository->getMasterList(
$testRowIds,
$request->query->getBoolean('applyRelations', TRUE),
$request->query->has('columns')
? explode(',', $request->query->get('columns'))
: []
);
$response = [
'map' => [],
'extracted' => [],
'list' => $list,
'total' => count($list)
];
$this->_compress($response);
$jsonResponse = new JsonResponse($response);
// compress output by gzip/deflate it
$jsonResponse->setContent($this->_encodeOutputIfBrowserSupports($jsonResponse, $jsonResponse->getContent()));
return $jsonResponse;
}
private function _extractLocations(array &$list, &$flippedMap, &$locationFieldsMap, &$locationFields2Remove) : array
{
$locations = [];
foreach ($list as &$item)
{
if (!key_exists(self::LOCATIONS_LITERAL, $item) || empty($item[self::LOCATIONS_LITERAL]))
{
unset($item[self::LOCATIONS_LITERAL]);
continue;
}
foreach ($item[self::LOCATIONS_LITERAL] as &$location)
{
$locationRowId = $location['location_row_id'];
if (!key_exists($locationRowId, $locations))
{
$n = count($locations);
$locations[$locationRowId] = Arr::extract($location, Arr::getPart($flippedMap, $locationFieldsMap));
} else {
$n = Arr::get(array_flip(array_keys($locations)), $locationRowId);
}
$location['lcid'] = $n;
Arr::forget($location, $locationFields2Remove);
}
}
return array_values($locations);
}
private function _makeMap(array $keys) : array
{
$getFirstLetter = function(string $str): string
{
return mb_substr($str, 0, 1, "UTF-8");
};
$result = [];
foreach ($keys as $n => $key)
{
$variant = implode('', array_map($getFirstLetter, explode('_', $key)));
if (key_exists($variant, $result) && $result[$variant] != $key)
{
$variant .= $n;
}
$result[$variant] = $key;
}
return $result;
}
private function _changeKeys(&$item, &$flippedMap)
{
foreach ($item as $key => $value)
{
$newKey = Arr::get($flippedMap, $key);
if (!$newKey)
{
continue;
}
$item[$newKey] = $value;
unset($item[$key]);
}
}
/**
* Experimental text compression (converts json field names => 1 letter)
*
* @param $response
*/
private function _compress(&$response)
{
$i = 0;
do {
$item = $response['list'][$i++];
} while (empty($item[self::LOCATIONS_LITERAL]));
$map = array_merge([], $this->_makeMap(array_keys($item)), $this->_makeMap(array_keys(reset($item[self::LOCATIONS_LITERAL]))));
$response['map'] = $map;
$flippedMap = array_flip($map);
$locationFieldsMap = [
'location_id',
'location_row_id',
'description'
];
$locationFields2Remove = array_merge($locationFieldsMap, ['widget_row_id']);
$response['extracted'] = [
'l' => [
'index' => 'lcid',
'list' => $this->_extractLocations(
$response['list'],
$flippedMap,
$locationFieldsMap,
$locationFields2Remove
)
]
];
foreach ($response['list'] as &$item)
{
if (key_exists('screenshot', $item))
{
unset($item['screenshot']);
}
$locations = Arr::get($item, self::LOCATIONS_LITERAL);
if (!empty($locations))
{
foreach ($item[self::LOCATIONS_LITERAL] as &$location)
{
$this->_changeKeys($location, $flippedMap);
}
}
$this->_changeKeys($item, $flippedMap);
}
}
/**
* Compresses output if browser supports
*
* @param Response $response
* @param string $content
* @return string
*/
private function _encodeOutputIfBrowserSupports(Response $response, string $content): string
{
//prefer deflate
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== false) {
$response->headers->set('Content-Encoding', 'deflate');
return gzdeflate($content);
}
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
$response->headers->set('Content-Encoding', 'gzip');
return gzencode($content);
}
return $content;
}
<?php
namespace RedactedBundle\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use RedactedBundle\Helper\Arr;
class ResponseCompressListener
{
const CONTENT_ENCODING_STR_LITERAL = 'Content-Encoding';
const TRANSFER_ENCODING_STR_LITERAL = 'Transfer-Encoding';
const COMPRESSION_FLAG_NAME = '_compression';
const COMPRESSION_METHODS = [
'deflate' => ZLIB_ENCODING_DEFLATE,
'gzip' => ZLIB_ENCODING_GZIP
];
/**
* @var bool
*/
private $kernelDebug;
/**
* @var Request
*/
private $request;
/**
* @var Response
*/
private $response;
/**
* @var Session
*/
private $session;
/**
* ResponseCompressListener constructor.
* @param bool $kernelDebug
* @param Session $session
*/
public function __construct(bool $kernelDebug, Session $session)
{
$this->kernelDebug = $kernelDebug;
$this->session = $session;
}
public function onKernelResponse(FilterResponseEvent $event)
{
$this->response = $event->getResponse();
$this->request = $event->getRequest();
$isResponseAlreadyEncoded = !empty($this->response->headers->get(self::CONTENT_ENCODING_STR_LITERAL));
if ($this->_isCompressionRequired() && !$isResponseAlreadyEncoded)
{
// compress output using gzip/deflate
$event->setResponse(
$this->response->setContent(
$this->_compressOutput()
)
);
}
}
/**
* @return bool
*/
private function _isCompressionRequired(): bool
{
$forceCompressionFlag = $this->request->get(self::COMPRESSION_FLAG_NAME);
if (!is_null($forceCompressionFlag))
{
$forceCompressionFlag = (bool)$forceCompressionFlag;
$this->session->set(self::COMPRESSION_FLAG_NAME, $forceCompressionFlag);
}
elseif ($this->session->has(self::COMPRESSION_FLAG_NAME))
{
$forceCompressionFlag = $this->session->get(self::COMPRESSION_FLAG_NAME);
}
return $forceCompressionFlag || !$this->kernelDebug;
}
/**
* @return array
*/
private function _supportedEncodings()
{
return array_intersect(
array_keys(self::COMPRESSION_METHODS),
$this->request->getEncodings()
);
}
/**
* @return mixed|null
*/
private function _getSupportedEncodingMethod()
{
$supportedEncodings = $this->_supportedEncodings();
if (empty($supportedEncodings))
{
return NULL;
}
return Arr::first($supportedEncodings);
}
/**
* @param string $method
* @param string $content
* @return string
*/
private function _encode(string $method, string $content):string
{
$this->response->headers->set(self::CONTENT_ENCODING_STR_LITERAL, $method);
$this->response->headers->set(self::TRANSFER_ENCODING_STR_LITERAL, $method);
return gzcompress($content, -1, self::COMPRESSION_METHODS[$method]);
}
/**
* Compresses output if browser supports
*
* @return string
*/
private function _compressOutput(): string
{
$supportedEncodingMethod = $this->_getSupportedEncodingMethod();
$content = $this->response->getContent();
if ($supportedEncodingMethod)
{
$content = $this->_encode(
$supportedEncodingMethod,
$content
);
}
return $content;
}
}
<?php
namespace Upwork\Bundle\SuitBundle\Helper;
use Closure;
use InvalidArgumentException;
use Traversable;
use function array_fill_keys;
use function array_map;
use function array_values;
use function array_walk;
use Doctrine\Common\Collections\Collection;
use function is_array;
use function is_null;
use function is_string;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use function strpos;
use Symfony\Component\Console\Helper\Helper;
/**
* Mix of illuminate/Support/Arr and Kohana's Arr helper
*
* Class ArrHelper
* @package Upwork\Bundle\SuitEventsDictionaryBundle\Helper
*/
class Arr extends Helper
{
private static $delimiter = '.';
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
*/
public function getName()
{
return 'arr';
}
/**
* Return the default value of the given value.
*
* @param mixed $value
* @return mixed
*/
protected static function value($value)
{
return $value instanceof Closure ? $value() : $value;
}
/**
* Determine whether the given value is array accessible.
*
* @param mixed $value
* @return bool
*/
public static function accessible($value)
{
return self::isArray($value);
}
/**
* Add an element to an array using "dot" notation if it doesn't exist.
*
* @param array $array
* @param string|array $key
* @param mixed $value
* @return array
*/
public static function add(&$array, $key, $value = NULL)
{
if (self::isArray($array) && is_null($value)) {
if (self::isArray($key)) {
array_walk($key, function($value) use (&$array){
array_push($array, $value);
});
}
elseif(is_string($key)) {
array_push($array, $key);
}
}
elseif (is_null(self::get($array, $key))) {
self::set($array, $key, $value);
}
return $array;
}
/**
* Collapse an array of arrays into a single array.
*
* @param iterable $array
* @return array
*/
public static function collapse($array)
{
$results = [];
foreach ($array as $values) {
if (!is_array($values)) {
continue;
}
$results[] = $values;
}
return array_merge([], ...$results);
}
/**
* Cross join the given arrays, returning all possible permutations.
*
* @param iterable ...$arrays
* @return array
*/
public static function crossJoin(...$arrays)
{
$results = [[]];
foreach ($arrays as $index => $array) {
$append = [];
foreach ($results as $product) {
foreach ($array as $item) {
$product[$index] = $item;
$append[] = $product;
}
}
$results = $append;
}
return $results;
}
/**
* Divide an array into two arrays. One with keys and the other with values.
*
* @param array $array
* @return array
*/
public static function divide($array)
{
return [array_keys($array), array_values($array)];
}
/**
* Flatten a multi-dimensional associative array with dots.
*
* @param iterable $array
* @param string $prepend
* @return array
*/
public static function dot($array, $prepend = '')
{
$results = [];
foreach ($array as $key => $value) {
if (is_array($value) && !empty($value)) {
$results = array_merge($results, self::dot($value, $prepend . $key . self::$delimiter));
} else {
$results[$prepend . $key] = $value;
}
}
return $results;
}
/**
* Get all of the given array except for a specified array of keys.
*
* @param array $array
* @param array|string $keys
* @param bool $byValue - use keys as array of values that should be missed in result array
* @return array
*/
public static function except($array, $keys, $byValue = FALSE)
{
if ($byValue)
return array_diff($array, $keys);
self::forget($array, $keys);
return $array;
}
/**
* Determine if the given key exists in the provided array.
*
* @param array $array
* @param string|int $key
* @return bool
*/
public static function exists($array, $key): bool
{
return array_key_exists($key, self::wrap($array));
}
/**
* @param mixed $array
* @param string|int|null $key
* @param string|int|null $compareTo
* @return bool
*/
public static function existsAndEqual($array, $key, $compareTo): bool
{
if (!self::isArray($array)) {
return FALSE;
}
return self::get($array, $key) == $compareTo;
}
/**
* Return the first element in an array passing a given truth test.
*
* @param iterable $array
* @param callable|null $callback
* @param mixed $default
* @return mixed
*/
public static function first($array, callable $callback = null, $default = null)
{
if (is_null($callback)) {
if (empty($array)) {
return self::value($default);
}
foreach ($array as $item) {
return $item;
}
}
foreach ($array as $key => $value) {
if ($callback($value, $key)) {
return $value;
}
}
return self::value($default);
}
/**
* Analog of array_map but with key of element passed as second parameter
*
* @param array $array
* @param string|callable $callback
* @param bool $removeEmpty
* @return mixed
*/
public static function map(array $array, $callback, $removeEmpty = FALSE): array
{
if (empty($array))
return [];
if (is_null($callback))
return $array;
$result = array_map($callback, $array, array_keys($array));
if (!$removeEmpty)
return $result;
return Arr::filterEmpty($result);
}
/**
* Return the last element in an array passing a given truth test.
*
* @param array $array
* @param callable|null $callback
* @param mixed $default
* @return mixed
*/
public static function last($array, callable $callback = null, $default = null)
{
if (is_null($callback))
return empty($array)
? self::value($default)
: end($array);
return self::first(array_reverse($array, true), $callback, $default);
}
/**
* Flatten a multi-dimensional array into a single level.
*
* @param iterable $array
* @param int $depth
* @return array
*/
public static function flatten($array, $depth = INF)
{
$result = [];
foreach ($array as $item) {
$item = $item instanceof Collection
? $item->toArray()
: $item;
if (!is_array($item)) {
$result[] = $item;
continue;
}
$values = $depth === 1
? array_values($item)
: self::flatten($item, $depth - 1);
foreach ($values as $value) {
$result[] = $value;
}
}
return $result;
}
/**
* Remove one or many array items from a given array using "dot" notation.
*
* @param array &$array
* @param array|string $keys
* @return array
*/
public static function forget(&$array, $keys)
{
$original = &$array;
$keys = self::wrap($keys);
if (!count($keys)) {
return $array;
}
foreach ($keys as $key) {
// if the exact key exists in the top-level, remove it
if (self::exists($array, $key)) {
unset($array[$key]);
continue;
}
$parts = explode(self::$delimiter, $key);
// clean up before each pass
$array = &$original;
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && is_array($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
}
}
unset($array[array_shift($parts)]);
}
return $array;
}
/**
* Get an item from an array using "dot" notation.
*
* @param array $array
* @param string|int|null $key
* @param mixed $default
* @return mixed
*/
public static function get($array, $key, $default = null)
{
if (!self::accessible($array)) {
return self::value($default);
}
if (is_null($key)) {
return $array;
}
if (self::exists($array, $key)) {
return $array[$key];
}
if (strpos($key, self::$delimiter) === false) {
return $array[$key] ?? self::value($default);
}
foreach (explode(self::$delimiter, $key) as $segment) {
if (self::accessible($array) && self::exists($array, $segment)) {
$array = $array[$segment];
} else {
return self::value($default);
}
}
return $array;
}
/**
* Check if an item or items exist in an array using "dot" notation.
*
* @param array $array
* @param string|array $keys
* @return bool
*/
public static function has($array, $keys)
{
$keys = self::wrap($keys);
if (!$array || empty($keys)) {
return FALSE;
}
foreach ($keys as $key) {
$subKeyArray = $array;
if (self::exists($array, $key)) {
continue;
}
foreach (explode(self::$delimiter, $key) as $segment) {
if (self::accessible($subKeyArray) && self::exists($subKeyArray, $segment)) {
$subKeyArray = $subKeyArray[$segment];
} else {
return FALSE;
}
}
}
return TRUE;
}
/**
* Determines if an array is associative.
*
* An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
*
* @param array $array
* @return bool
*/
public static function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @param array|string $keys
* @return array
*/
public static function only($array, $keys)
{
return array_intersect_key($array, array_flip((array)$keys));
}
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @return array
*/
public static function filterEmpty($array)
{
return array_filter($array, function($value) {
return !empty($value);
});
}
/**
* Get a subset of the items from the given array.
*
* @param array $array
* @param $key
* @param $val
* @param bool $contrariwise
* @return array
*/
public static function filterBy($array, $key, $val, $contrariwise = FALSE)
{
if (!self::isArray($array))
return [];
return array_filter($array, function($data) use ($key, $val, $contrariwise) {
$result = !!Arr::existsAndEqual($data, $key, $val);
return $contrariwise ? !$result : $result;
});
}
/**
* Pluck an array of values from an array.
*
* @param iterable $array
* @param string|array $value
* @param string|array|null $key
* @return array
*/
public static function pluck($array, $value, $key = null)
{
$results = [];
[$value, $key] = self::explodePluckParameters($value, $key);
foreach ($array as $item)
{
$itemValue = self::path($item, $value);
// If the key is "null", we will just append the value to the array and keep
// looping. Otherwise we will key the array using the value of the key we
// received from the developer. Then we'll return the final array form.
if (is_null($key)) {
$results[] = $itemValue;
continue;
}
$itemKey = self::path($item, $key);
if (is_object($itemKey) && method_exists($itemKey, '__toString')) {
$itemKey = (string)$itemKey;
}
$results[$itemKey] = $itemValue;
}
return $results;
}
/**
* Explode the "value" and "key" arguments passed to "pluck".
*
* @param string|array $value
* @param string|array|null $key
* @return array
*/
protected static function explodePluckParameters($value, $key)
{
$value = is_string($value) ? explode(self::$delimiter, $value) : $value;
$key = is_null($key) || is_array($key) ? $key : explode(self::$delimiter, $key);
return [$value, $key];
}
/**
* Push an item onto the beginning of an array.
*
* @param array $array
* @param mixed $value
* @param mixed $key
* @return array
*/
public static function prepend($array, $value, $key = null)
{
if (is_null($key)) {
array_unshift($array, $value);
} else {
$array = [$key => $value] + $array;
}
return $array;
}
/**
* Get a value from the array, and remove it.
*
* @param array $array
* @param string $key
* @param mixed $default
* @return mixed
*/
public static function pull(&$array, $key, $default = null)
{
$value = self::get($array, $key, $default);
self::forget($array, $key);
return $value;
}
/**
* Get one or a specified number of random values from an array.
*
* @param array $array
* @param int|null $number
* @return mixed
*
* @throws InvalidArgumentException
*/
public static function random($array, $number = null)
{
$requested = is_null($number) ? 1 : $number;
$count = count($array);
if ($requested > $count) {
throw new InvalidArgumentException(
"You requested {$requested} items, but there are only {$count} items available."
);
}
if (is_null($number)) {
return $array[array_rand($array)];
}
if ((int)$number === 0) {
return [];
}
$keys = array_rand($array, $number);
$results = [];
foreach ((array)$keys as $key) {
$results[] = $array[$key];
}
return $results;
}
protected static function hasDelimiter($str)
{
return strpos($str, self::$delimiter) > -1;
}
/**
* Set an array item to a given value using "dot" notation.
*
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string|array $key
* @param mixed $value
* @return array
*/
public static function set(&$array, $key, $value = null)
{
if (is_null($key)) {
return $array = $value;
}
//batch mode
if (self::isArray($key) && is_null($value) && self::isArray($array)) {
$batch = $key;
foreach ($batch as $key => $value)
self::set($array, $key, $value);
return $array;
}
if (!self::hasDelimiter($key)) {
$array[$key] = $value;
return $array;
}
$keys = explode(self::$delimiter, $key);
while (count($keys) > 1) {
$key = array_shift($keys);
// If the key doesn't exist at this depth, we will just create an empty array
// to hold the next value, allowing us to create the arrays to hold final
// values at the correct depth. Then we'll keep digging into the array.
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[array_shift($keys)] = $value;
return $array;
}
/**
* Shuffle the given array and return the result.
*
* @param array $array
* @param int|null $seed
* @return array
*/
public static function shuffle($array, $seed = null)
{
if (is_null($seed)) {
shuffle($array);
} else {
mt_srand($seed);
shuffle($array);
mt_srand();
}
return $array;
}
/**
* Recursively sort an array by keys and values.
*
* @param array $array
* @return array
*/
public static function sortRecursive($array)
{
foreach ($array as &$value) {
if (is_array($value)) {
$value = self::sortRecursive($value);
}
}
if (self::isAssoc($array)) {
ksort($array);
} else {
sort($array);
}
return $array;
}
/**
* Convert the array into a query string.
*
* @param array $array
* @return string
*/
public static function query($array)
{
return http_build_query($array, null, '&', PHP_QUERY_RFC3986);
}
/**
* Filter the array using the given criteria array
*
* @param array $array
* @param array $filterItems
* @param bool $contrariwise
* @return array
*/
public static function where($array, array $filterItems, $contrariwise = FALSE)
{
$result = [];
foreach($array as $_key => $element)
foreach ($filterItems as $key => $value)
{
$tmpVal = self::get($element, $key);
if ($contrariwise) {
if (self::isArray($value) && (is_null($tmpVal) || !in_array($tmpVal, $value))) {
$result[$_key] = $element;
} elseif ($tmpVal !== $value) {
$result[$_key] = $element;
}
continue;
}
if ($tmpVal === $value || (!is_null($tmpVal) && self::isArray($value) && in_array($tmpVal, $value))) {
$result[$_key] = $element;
}
}
return $result;
}
/**
* Check
*
* @param array $array
* @param $path
* @param $value
* @return bool
*/
public static function inArray(array $array, $path, $value): bool
{
if (self::isArray($value)) {
$array = $array[$path] ?? [];
foreach($array as $item)
{
if (Arr::isEqual($item, $value))
{
return TRUE;
}
}
return FALSE;
}
if (!self::hasDelimiter($path)) {
return in_array($value, $array[$path] ?? []);
}
return in_array($value, self::get($array, $path, []));
}
/**
* Find first element of collection where field $fieldName value equals $value
*
* @param array $array
* @param $fieldName
* @param $value
* @return array
*/
public static function findBy(array $array, $fieldName, $value)
{
return self::first(self::where($array, [
$fieldName => $value
]));
}
/**
* Filter the array using the given criteria array
*
* @param array $array
* @param $fieldName
* @param $value
* @return string|integer
*/
public static function findKeyBy($array, $fieldName, $value)
{
return self::first(array_keys(self::where($array, [
$fieldName => $value
])));
}
/**
* If the given value is not an array and not null, wrap it in one.
*
* @param mixed $value
* @return array
*/
public static function wrap($value): array
{
if (is_null($value)) {
return [];
}
return is_array($value) ? $value : [$value];
}
/**
* Determine whether the given value is array
*
* @param mixed $value
* @return bool
*/
public static function isArray($value)
{
if (is_array($value)) {
// Definitely an array
return TRUE;
} else {
// Possibly a Traversable object, functionally the same as an array
return (is_object($value) AND $value instanceof Traversable);
}
}
/**
* Determine whether the given value is associative array
*
* @param array $array
* @return bool
*/
public static function is_assoc(array $array)
{
// Keys of the array
$keys = array_keys($array);
// If the array keys of the keys match the keys, then the array must
// not be associative (e.g. the keys array looked like {0:0, 1:1...}).
return array_keys($keys) !== $keys;
}
/**
* Gets a value from an array using a dot separated path.
*
* @param array $array required - Array to search
* @param mixed $path required - Key path string (delimiter separated) or array of keys
* @param mixed $default = NULL - Default value if the path is not set
* @param string $delimiter = NULL - Key path delimiter
* @return array|null
*/
public static function path($array, $path, $default = NULL, $delimiter = NULL)
{
if (!self::isArray($array)) {
// This is not an array!
return $default;
}
if (is_array($path)) {
// The path has already been separated into keys
$keys = $path;
} else {
if (array_key_exists($path, $array)) {
// No need to do extra processing
return $array[$path];
}
if ($delimiter === NULL) {
// Use the default delimiter
$delimiter = self::$delimiter;
}
// Remove starting delimiters and spaces
$path = ltrim($path, "{$delimiter} ");
// Remove ending delimiters, spaces, and wildcards
$path = rtrim($path, "{$delimiter} *");
// Split the keys by delimiter
$keys = explode($delimiter, $path);
}
do {
$key = array_shift($keys);
if (ctype_digit($key)) {
// Make the key an integer
$key = (int)$key;
}
if (isset($array[$key])) {
if (!$keys) {
// Found the path requested
return $array[$key];
}
if (!self::isArray($array[$key])) {
// Unable to dig deeper
break;
}
// Dig down into the next part of the path
$array = $array[$key];
} elseif ($key === '*') {
// Handle wildcards
$values = array();
foreach ($array as $arr) {
if ($value = self::path($arr, implode(self::$delimiter, $keys))) {
$values[] = $value;
}
}
if (!$values) {
// Unable to dig deeper
break;
}
// Found the values requested
return $values;
} else {
// Unable to dig deeper
break;
}
} while ($keys);
// Unable to find the value requested
return $default;
}
/**
* @param $array
* @param array $paths
* @param null $default
* @param null $delimiter
* @return array|null
*/
public static function pick($array, $paths = [], $default = NULL, $delimiter = NULL)
{
if (!self::isArray($array)) {
// This is not an array!
return $default;
}
if (is_string($paths)) {
return Arr::pluck($array, $paths);
}
if ($delimiter === NULL) {
// Use the default delimiter
$delimiter = self::$delimiter;
}
$array = array_values($array);
$result = [];
foreach($array as $key => $item) {
$result[$key] = [];
foreach($paths as $path)
$result[$key][$path] = self::path($item, $path, $default, $delimiter);
}
return $result;
}
/**
* @param $array
* @param array $keys
* @return array
*/
public static function getPart($array, $keys = [])
{
$result = [];
foreach ($keys as $key) {
$result[$key] = self::get($array, $key);
}
return $result;
}
/**
* Function that groups an array of associative arrays by some key.
*
* @param {String} $key Property to sort by.
* @param {Array} $data Array that stores multiple associative arrays.
* @return array
*/
public static function groupBy($data, $key) {
$result = [];
// todo: add "." (dot) path key format like in self::path
foreach($data as $index => $val) {
if(!self::has($val, $key)) {
if (is_string($index)) {
$result[''][$index] = $val;
} else {
$result[''][] = $val;
}
continue;
}
$result[$val[$key]][] = $val;
}
return $result;
}
public static function diffRecursive($firstArray, $secondArray, $reverseKey = false)
{
$oldKey = 'old';
$newKey = 'new';
if ($reverseKey) {
$oldKey = 'new';
$newKey = 'old';
}
$difference = [];
foreach ($firstArray as $firstKey => $firstValue) {
$secondArrayFirstKeyVal = self::get($secondArray, $firstKey);
if (self::isArray($firstValue)) {
if (!key_exists($firstKey, $secondArray) || !self::isArray($secondArrayFirstKeyVal)) {
self::set($difference, "$oldKey.$firstKey", $firstValue);
self::set($difference, "$newKey.$firstKey", '');
} else {
$newDiff = self::diffRecursive($firstValue, self::get($secondArray, $firstKey), $reverseKey);
if (!empty($newDiff)) {
self::set($difference, "$oldKey.$firstKey", self::get($newDiff, $oldKey));
self::set($difference, "$newKey.$firstKey", self::get($newDiff, $newKey));
}
}
} else if (!key_exists($firstKey, $secondArray) || $secondArrayFirstKeyVal != $firstValue) {
self::set($difference, "$oldKey.$firstKey", $firstValue);
self::set($difference, "$newKey.$firstKey", $secondArrayFirstKeyVal);
}
}
return $difference;
}
/**
* @param array $array1
* @param array $array2
* @return array
*/
public static function compare(array $array1, array $array2): array
{
return array_replace_recursive(
self::diffRecursive($array1, $array2),
self::diffRecursive($array2, $array1, true)
);
}
/**
* @param array $array1
* @param array $array2
* @return bool
*/
public static function isEqual(array $array1, array $array2): bool
{
return !(bool)count(self::compare($array1, $array2));
}
/**
* @param array $source
* @param array $keysMap
* @return array
*/
public static function extract($source, $keysMap): array
{
/*
$keysMap format:
[
'search key name' => 'new key name',
...
]
this function will extract values for `search key name` keys from
$source array and change it key names to new key names
*/
$source = Arr::wrap($source);
$keysMap = Arr::wrap($keysMap);
if (!Arr::is_assoc($source) || !Arr::is_assoc($keysMap))
return [];
return Arr::collapse(Arr::map($keysMap, function($val, $key) use ($source) {
return [$val => Arr::get($source, $key)];
}));
}
/**
*
*
* @param array $source
* @return array
*/
public static function buildPaths(array $source): array
{
/*
this function convenient to use with `Arr::get` or `Arr::path` function
it builds flatten array of each non array node of $source array with path to its value
```
$source =
[
'key1' => 'some 1value',
'key2' => 'some 2value',
'key3' => [
'key13' => '13value',
'key23' => [
'key123' => '123value'
]
],
'some_key_name' => 'some value',
...
]
returns:
[
'key1' => 'some 1value',
'key2' => 'some 2value',
'key3.key13' => '13value',
'key3.key23.key123' => '123value',
'some_key_name' => 'some value'
]
```
*/
$source = Arr::wrap($source);
$iterator = new RecursiveIteratorIterator(
new RecursiveArrayIterator($source),
RecursiveIteratorIterator::SELF_FIRST
);
$result = [];
foreach ($iterator as $key => $val) {
if (!is_array($val)) {
// add the current key
$keys = array($key);
// loop up the recursive chain
$i = $iterator->getDepth() - 1;
for(; $i >= 0; ) {
// add each parent key
array_unshift($keys, $iterator->getSubIterator($i--)->key());
}
$result[implode('.', $keys)] = $val;
}
}
return $result;
}
/**
* Prepares $config to $container->setParameter function
*
*
* @param array $config
* @param array $skip
* @return array
*/
public static function config2parameters(array $config, $skip = []): array
{
$paths = Arr::buildPaths($config);
$skip = array_fill_keys($skip, []);
$items = [];
$result = array_filter($paths, function($v, $k) use ($skip, &$items) {
foreach ($skip as $key => $value) {
if (strpos($k, $key) !== FALSE) {
Arr::set($items, $k, $v);
return FALSE;
}
}
return TRUE;
}, ARRAY_FILTER_USE_BOTH);
foreach($skip as $key => &$value) {
$value = Arr::get($items, $key, []);
}
return array_merge([], $result, $skip);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment