Created
September 15, 2021 19:32
-
-
Save santaklouse/77b10ae01906fdaf96ea74fea6b42a63 to your computer and use it in GitHub Desktop.
Simple compression of json API response for js table (Experiment)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| transformResponse: (data, headersGetter, status) => { | |
| if (status >= 300) { | |
| return {}; | |
| } | |
| // decompressing | |
| return MappedJSON.decompress(angular.fromJson(data)); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .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 | |
| } | |
| }]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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