/** * @internal * * @param array $request * * @return array */ public function _invokeAsArray(array $request) { $factory = $this->factory; // Ensure headers are by reference. They're updated elsewhere. $result = $factory($request, curl_init()); $h = $result[0]; $hd =& $result[1]; $body = $result[2]; Core::doSleep($request); try { // override the default body stream with the request response $safecurl = new SafeCurl($h); $body = $safecurl->execute(Core::url($request)); } catch (Exception $e) { // URL wasn't safe, return empty content $body = ''; $safeCurlError = $e->getMessage(); } $response = ['transfer_stats' => curl_getinfo($h)]; $response['curl']['error'] = curl_error($h); $response['curl']['errno'] = curl_errno($h); $response['transfer_stats'] = array_merge($response['transfer_stats'], $response['curl']); curl_close($h); // override default error message in case of SafeCurl error if (isset($safeCurlError)) { $response['err_message'] = $safeCurlError; } return CurlFactory::createResponse([$this, '_invokeAsArray'], $request, $response, $hd, Stream::factory($body)); }
/** * Get all of the received requests as a RingPHP request structure. * * @return array * @throws \RuntimeException */ public static function received() { if (!self::$started) { return []; } $response = self::send('GET', '/guzzle-server/requests'); $body = Core::body($response); $result = json_decode($body, true); if ($result === false) { throw new \RuntimeException('Error decoding response: ' . json_last_error()); } foreach ($result as &$res) { if (isset($res['uri'])) { $res['resource'] = $res['uri']; } if (isset($res['query_string'])) { $res['resource'] .= '?' . $res['query_string']; } if (!isset($res['resource'])) { $res['resource'] = ''; } // Ensure that headers are all arrays if (isset($res['headers'])) { foreach ($res['headers'] as &$h) { $h = (array) $h; } unset($h); } } unset($res); return $result; }
public function valid() { get_next: // Return true if this function has already been called for iteration. if ($this->currentRequest) { return true; } // Return false if we are at the end of the provided commands iterator. if (!$this->commands->valid()) { return false; } $command = $this->commands->current(); if (!$command instanceof CommandInterface) { throw new \RuntimeException('All commands provided to the ' . __CLASS__ . ' must implement GuzzleHttp\\Command\\CommandInterface.' . ' Encountered a ' . Core::describeType($command) . ' value.'); } $command->setFuture('lazy'); $this->attachListeners($command, $this->eventListeners); // Prevent transfer exceptions from throwing. $command->getEmitter()->on('process', function (ProcessEvent $e) { if ($e->getException()) { $e->setResult(null); } }, RequestEvents::LATE); $builder = $this->requestBuilder; $result = $builder($command); // Skip commands that were intercepted with a result. if (isset($result['result'])) { $this->commands->next(); goto get_next; } $this->currentRequest = $result['request']; return true; }
public function wait() { $result = $this->parentWait(); if (!$result instanceof ResultInterface) { throw new \RuntimeException('Expected a ResultInterface. Found ' . Core::describeType($result)); } return $result; }
public function __invoke(array $request) { Core::doSleep($request); $response = is_callable($this->result) ? call_user_func($this->result, $request) : $this->result; if (is_array($response)) { $response = new CompletedFutureArray($response + array('status' => null, 'body' => null, 'headers' => array(), 'reason' => null, 'effective_url' => null)); } elseif (!$response instanceof FutureArrayInterface) { throw new \InvalidArgumentException('Response must be an array or FutureArrayInterface. Found ' . Core::describeType($request)); } return $response; }
/** * @param string $url * * @return boolean * * @throws \RuntimeException */ public static function download($url) { $handler = new Ring\Client\CurlHandler(); $response = $handler(['http_method' => 'GET', 'uri' => sprintf(':%s/%s', parse_url($url, PHP_URL_PORT), parse_url($url, PHP_URL_PATH)), 'headers' => ['scheme' => [parse_url($url, PHP_URL_SCHEME)], 'host' => [parse_url($url, PHP_URL_HOST)]]]); $response->wait(); if ($response['status'] != 200) { throw new \RuntimeException(sprintf('%s: %s (%s)', $response['effective_url'], $response['reason'], $response['status'])); } $json = Ring\Core::body($response); return self::load($json); }
public function __invoke(array $request) { $factory = $this->factory; // Ensure headers are by reference. They're updated elsewhere. $result = $factory($request, $this->checkoutEasyHandle()); $h = $result[0]; $hd =& $result[1]; $bd = $result[2]; Core::doSleep($request); curl_exec($h); $response = ['transfer_stats' => curl_getinfo($h)]; $response['curl']['error'] = curl_error($h); $response['curl']['errno'] = curl_errno($h); $this->releaseEasyHandle($h); return new CompletedFutureArray(CurlFactory::createResponse($this, $request, $response, $hd, $bd)); }
public function __invoke(array $request) { Core::doSleep($request); $response = is_callable($this->result) ? call_user_func($this->result, $request) : $this->result; $data = "'" . Stream::factory($request['body']) . "'"; $headers = ''; foreach ($request['headers'] as $key => $val) { $headers .= '-H "' . $key . ': ' . $val[0] . '" '; } $exec = 'curl -X POST ' . trim($headers) . ' -d ' . $data . ' "' . $request['url'] . '"'; $exec .= " >/dev/null 2>&1 &"; exec($exec, $output, $return_var); if (is_array($response)) { $response = new CompletedFutureArray($response + ['status' => 200, 'body' => null, 'headers' => [], 'reason' => null, 'effective_url' => null]); } elseif (!$response instanceof FutureArrayInterface) { throw new \InvalidArgumentException('Response must be an array or FutureArrayInterface. Found ' . Core::describeType($request)); } return $response; }
/** * Fetches the HTTP URL * * @api * @param string $url * @param array $options See Guzzle Ring Request * @return array Ring Response */ function fetch($url, array $options = []) { $urlComponents = parse_url($url); $request = []; $options = _default_options($options); if (isset($options['handler'])) { $handler = $options['handler']; unset($options['handler']); } else { $handler = default_handler(); } $request = array_merge(['uri' => isset($urlComponents['path']) ? $urlComponents['path'] : '/', 'scheme' => $urlComponents['scheme']], $options); if (isset($urlComponents['query'])) { $request['query_string'] = $urlComponents['query']; } if (!Core::hasHeader($request, 'host')) { $host = $urlComponents['host']; if (isset($urlComponents['port']) && $urlComponents['port'] !== 80 && $urlComponents['port'] !== 443) { $host .= ':' . $urlComponents['port']; } $request['headers']['host'] = [$host]; } if (!Core::hasHeader($request, 'authorization')) { if (isset($urlComponents['user'])) { $user = $urlComponents['user']; $password = isset($urlComponents['pass']) ? $urlComponents['pass'] : ''; } elseif (isset($options['auth'])) { @(list($user, $password) = $options['auth']); } if (isset($user) && isset($password)) { $request['headers']['authorization'] = ['Basic ' . base64_encode($user . ':' . $password)]; } } $response = $handler($request); return $response; }
/** * Set the query part of the URL * * @param Query|string|array $query Query string value to set. Can * be a string that will be parsed into a Query object, an array * of key value pairs, or a Query object. * * @throws \InvalidArgumentException */ public function setQuery($query) { if ($query instanceof Query) { $this->query = $query; } elseif (is_string($query)) { $this->query = Query::fromString($query); } elseif (is_array($query)) { $this->query = new Query($query); } else { throw new \InvalidArgumentException('Query must be a Query, ' . 'array, or string. Got ' . Core::describeType($query)); } }
private function add_debug(array $request, &$options, $value, &$params) { if ($value === false) { return; } static $map = array(STREAM_NOTIFY_CONNECT => 'CONNECT', STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', STREAM_NOTIFY_PROGRESS => 'PROGRESS', STREAM_NOTIFY_FAILURE => 'FAILURE', STREAM_NOTIFY_COMPLETED => 'COMPLETED', STREAM_NOTIFY_RESOLVE => 'RESOLVE'); static $args = array('severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'); $value = Core::getDebugResource($value); $ident = $request['http_method'] . ' ' . Core::url($request); $fn = function () use($ident, $value, $map, $args) { $passed = func_get_args(); $code = array_shift($passed); fprintf($value, '<%s> [%s] ', $ident, $map[$code]); foreach (array_filter($passed) as $i => $v) { fwrite($value, $args[$i] . ': "' . $v . '" '); } fwrite($value, "\n"); }; // Wrap the existing function if needed. $params['notification'] = isset($params['notification']) ? Core::callArray([$params['notification'], $fn]) : $fn; }
public function testSupports100Continue() { Server::flush(); Server::enqueue([['status' => '200', 'reason' => 'OK', 'headers' => ['Test' => ['Hello'], 'Content-Length' => ['4']], 'body' => 'test']]); $request = ['http_method' => 'PUT', 'headers' => ['Host' => [Server::$host], 'Expect' => ['100-Continue']], 'body' => 'test']; $handler = new StreamHandler(); $response = $handler($request); $this->assertEquals(200, $response['status']); $this->assertEquals('OK', $response['reason']); $this->assertEquals(['Hello'], $response['headers']['Test']); $this->assertEquals(['4'], $response['headers']['Content-Length']); $this->assertEquals('test', Core::body($response)); }
/** * This function ensures that a response was set on a transaction. If one * was not set, then the request is retried if possible. This error * typically means you are sending a payload, curl encountered a * "Connection died, retrying a fresh connect" error, tried to rewind the * stream, and then encountered a "necessary data rewind wasn't possible" * error, causing the request to be sent through curl_multi_info_read() * without an error status. */ private static function retryFailedRewind(callable $handler, array $request, array $response) { // If there is no body, then there is some other kind of issue. This // is weird and should probably never happen. if (!isset($request['body'])) { $response['err_message'] = 'No response was received for a request ' . 'with no body. This could mean that you are saturating your ' . 'network.'; return self::createErrorResponse($handler, $request, $response); } if (!Core::rewindBody($request)) { $response['err_message'] = 'The connection unexpectedly failed ' . 'without providing an error. The request would have been ' . 'retried, but attempting to rewind the request body failed.'; return self::createErrorResponse($handler, $request, $response); } // Retry no more than 3 times before giving up. if (!isset($request['curl']['retries'])) { $request['curl']['retries'] = 1; } elseif ($request['curl']['retries'] == 2) { $response['err_message'] = 'The cURL request was retried 3 times ' . 'and did no succeed. cURL was unable to rewind the body of ' . 'the request and subsequent retries resulted in the same ' . 'error. Turn on the debug option to see what went wrong. ' . 'See https://bugs.php.net/bug.php?id=47204 for more information.'; return self::createErrorResponse($handler, $request, $response); } else { $request['curl']['retries']++; } return $handler($request); }
public function testProxiesDeferredFutureFailure() { $d = new Deferred(); $f = new FutureArray($d->promise()); $f2 = Core::proxy($f); $d->reject(new \Exception('foo')); try { $f2['hello?']; $this->fail('did not throw'); } catch (\Exception $e) { $this->assertEquals('foo', $e->getMessage()); } }
/** * Adds the next request to pool and tracks what requests need to be * dereferenced when completing the pool. */ private function addNextRequest() { if ($this->isRealized || !$this->iter || !$this->iter->valid()) { return false; } $request = $this->iter->current(); $this->iter->next(); if (!$request instanceof RequestInterface) { throw new \InvalidArgumentException(sprintf('All requests in the provided iterator must implement ' . 'RequestInterface. Found %s', Core::describeType($request))); } // Be sure to use "lazy" futures, meaning they do not send right away. $request->getConfig()->set('future', 'lazy'); $this->attachListeners($request, $this->eventListeners); $response = $this->client->send($request); $hash = spl_object_hash($request); $this->waitQueue[$hash] = $response; // Use this function for both resolution and rejection. $fn = function ($value) use($request, $hash) { unset($this->waitQueue[$hash]); $result = $value instanceof ResponseInterface ? ['request' => $request, 'response' => $value, 'error' => null] : ['request' => $request, 'response' => null, 'error' => $value]; $this->deferred->progress($result); $this->addNextRequest(); }; $response->then($fn, $fn); return true; }
public function testMaintainsMultiHeaderOrder() { Server::flush(); Server::enqueue([['status' => 200, 'headers' => ['Content-Length' => ['0'], 'Foo' => ['a', 'b'], 'foo' => ['c', 'd']]]]); $a = new CurlMultiHandler(); $response = $a(['http_method' => 'GET', 'headers' => ['Host' => [Server::$host]]])->wait(); $this->assertEquals(['a', 'b', 'c', 'd'], Core::headerLines($response, 'Foo')); }
private function configureDefaults($config) { if (!isset($config['defaults'])) { $this->defaults = $this->getDefaultOptions(); } else { $this->defaults = array_replace($this->getDefaultOptions(), $config['defaults']); } // Add the default user-agent header if (!isset($this->defaults['headers'])) { $this->defaults['headers'] = array('User-Agent' => static::getDefaultUserAgent()); } elseif (!Core::hasHeader($this->defaults, 'User-Agent')) { // Add the User-Agent header if one was not already set $this->defaults['headers']['User-Agent'] = static::getDefaultUserAgent(); } }
/** * @param callable $handler * @param LoggerInterface $logger * @return callable */ public static function processResponse(callable $handler, LoggerInterface $logger) { return function (array $request) use($handler, $logger) { return Core::proxy($handler($request), function (array $response) use($request, $logger) { // Is there any error? if (isset($response['error'])) { $exception = new TransportException('Request failure', 0, $response['error']); Logger::logState($logger, $request, $response, $exception, LogLevel::CRITICAL); throw $exception; } $response['json'] = null; // Read body if (isset($response['body']) && is_resource($response['body'])) { $response['body'] = stream_get_contents($response['body']); // false if something wrong // Extract json data if ($response['body']) { $response['json'] = json_decode($response['body'], true); // null if something wrong } } // Process errors if ($response['status'] >= 400) { $ignore = isset($request['client']['ignore']) ? (array) $request['client']['ignore'] : []; // It is possible to ignore some status codes if (!in_array($response['status'], $ignore)) { // Is there any message? $message = isset($response['json']['message']) ? $response['json']['message'] : 'Unknown error'; if ($response['status'] >= 400 && $response['status'] < 500) { if (404 == $response['status']) { $exception = new ResourceNotFoundException(); } else { $exception = new BadRequestException($message, $response['status']); } } else { $exception = new ServerException($message, $response['status']); } Logger::logState($logger, $request, $response, $exception, LogLevel::ERROR); throw $exception; } } Logger::logState($logger, $request, $response); return $response; }); }; }
private function getDefaultOptions(array $request) { $headers = ""; foreach ($request['headers'] as $name => $value) { foreach ((array) $value as $val) { $headers .= "{$name}: {$val}\r\n"; } } $context = ['http' => ['method' => $request['http_method'], 'header' => $headers, 'protocol_version' => isset($request['version']) ? $request['version'] : 1.1, 'ignore_errors' => true, 'follow_location' => 0]]; $body = Core::body($request); if (isset($body)) { $context['http']['content'] = $body; // Prevent the HTTP handler from adding a Content-Type header. if (!self::hasHeader($request, 'Content-Type')) { $context['http']['header'] .= "Content-Type:\r\n"; } } $context['http']['header'] = rtrim($context['http']['header']); return $context; }
private function wrapHandler(callable $handler, LoggerInterface $logger, LoggerInterface $tracer) { return function (array $request, Connection $connection, Transport $transport = null, $options) use($handler, $logger, $tracer) { $this->lastRequest = ['request' => $request]; // Send the request using the wrapped handler. $response = Core::proxy($handler($request), function ($response) use($connection, $transport, $logger, $tracer, $request, $options) { $this->lastRequest['response'] = $response; if (isset($response['error'])) { if ($response['error'] instanceof ConnectException || $response['error'] instanceof RingException) { $this->log->warning('Curl exception encountered.'); $exception = $this->getCurlRetryException($request, $response); $this->logRequestFail($request['http_method'], $response['effective_url'], $request['body'], $request['headers'], $response['status'], $response['body'], $response['transfer_stats']['total_time'], $exception); $node = $connection->getHost(); $this->log->warning("Marking node {$node} dead."); $connection->markDead(); // If the transport has not been set, we are inside a Ping or Sniff, // so we don't want to retrigger retries anyway. // // TODO this could be handled better, but we are limited because connectionpools do not // have access to Transport. Architecturally, all of this needs to be refactored if (isset($transport)) { $transport->connectionPool->scheduleCheck(); $neverRetry = isset($request['client']['never_retry']) ? $request['client']['never_retry'] : false; $shouldRetry = $transport->shouldRetry($request); $shouldRetryText = $shouldRetry ? 'true' : 'false'; $this->log->warning("Retries left? {$shouldRetryText}"); if ($shouldRetry && !$neverRetry) { return $transport->performRequest($request['http_method'], $request['uri'], [], $request['body'], $options); } } $this->log->warning("Out of retries, throwing exception from {$node}"); // Only throw if we run out of retries throw $exception; } else { // Something went seriously wrong, bail $exception = new TransportException($response['error']->getMessage()); $this->logRequestFail($request['http_method'], $response['effective_url'], $request['body'], $request['headers'], $response['status'], $response['body'], $response['transfer_stats']['total_time'], $exception); throw $exception; } } else { $connection->markAlive(); if (isset($response['body']) === true) { $response['body'] = stream_get_contents($response['body']); $this->lastRequest['response']['body'] = $response['body']; } if ($response['status'] >= 400 && $response['status'] < 500) { $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; $this->process4xxError($request, $response, $ignore); } elseif ($response['status'] >= 500) { $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; $this->process5xxError($request, $response, $ignore); } // No error, deserialize $response['body'] = $this->serializer->deserialize($response['body'], $response['transfer_stats']); } $this->logRequestSuccess($request['http_method'], $response['effective_url'], $request['body'], $request['headers'], $response['status'], $response['body'], $response['transfer_stats']['total_time']); return isset($request['client']['verbose']) && $request['client']['verbose'] === true ? $response : $response['body']; }); return $response; }; }
/** * Throw when an invalid type is encountered. * * @param string $name Name of the value being validated. * @param mixed $provided The provided value. * * @throws \InvalidArgumentException */ private function invalidType($name, $provided) { $expected = implode('|', $this->argDefinitions[$name]['valid']); $msg = "Invalid configuration value " . "provided for \"{$name}\". Expected {$expected}, but got " . Core::describeType($provided) . "\n\n" . $this->getArgMessage($name); throw new \InvalidArgumentException($msg); }
/** * Creates a Guzzle request object using a ring request array. * * @param array $request Ring request * * @return Request * @throws \InvalidArgumentException for incomplete requests. */ public static function fromRingRequest(array $request) { $options = []; if (isset($request['version'])) { $options['protocol_version'] = $request['version']; } if (!isset($request['http_method'])) { throw new \InvalidArgumentException('No http_method'); } return new Request($request['http_method'], Core::url($request), isset($request['headers']) ? $request['headers'] : [], isset($request['body']) ? Stream::factory($request['body']) : null, $options); }
private function wrapHandler(callable $handler, LoggerInterface $logger, LoggerInterface $tracer) { return function (array $request, Connection $connection, Transport $transport = null, $options) use($handler, $logger, $tracer) { $this->lastRequest = []; $this->lastRequest['request'] = $request; // Send the request using the wrapped handler. $response = Core::proxy($handler($request), function ($response) use($connection, $transport, $logger, $tracer, $request, $options) { $this->lastRequest['response'] = $response; if (isset($response['error']) === true) { if ($response['error'] instanceof ConnectException || $response['error'] instanceof RingException) { $connection->markDead(); $transport->connectionPool->scheduleCheck(); $neverRetry = isset($request['client']['never_retry']) ? $request['client']['never_retry'] : false; $shouldRetry = $transport->shouldRetry($request); if ($shouldRetry && !$neverRetry) { return $transport->performRequest($request['http_method'], $request['uri'], [], $request['body'], $options); } // Due to the magic of futures, this will only be invoked if the final retry fails, since // successful resolutions will go down the alternate `else` path the second time through // the proxy $this->throwCurlException($request, $response); } else { // Something went seriously wrong, bail throw new TransportException($response['error']->getMessage()); } } else { $connection->markAlive(); if (isset($response['body']) === true) { $response['body'] = stream_get_contents($response['body']); $this->lastRequest['response']['body'] = $response['body']; } if ($response['status'] >= 400 && $response['status'] < 500) { $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; $this->process4xxError($request, $response, $ignore); } elseif ($response['status'] >= 500) { $ignore = isset($request['client']['ignore']) ? $request['client']['ignore'] : []; $this->process5xxError($request, $response, $ignore); } // No error, deserialize $response['body'] = $this->serializer->deserialize($response['body'], $response['transfer_stats']); } $this->logRequestSuccess($request['http_method'], $response['effective_url'], $request['body'], $request['headers'], $response['status'], $response['body'], $response['transfer_stats']['total_time']); return isset($request['client']['verbose']) && $request['client']['verbose'] === true ? $response : $response['body']; }); return $response; }; }
/** * Set the query part of the URL. * * You may provide a query string as a string and pass $rawString as true * to provide a query string that is not parsed until a call to getQuery() * is made. Setting a raw query string will still encode invalid characters * in a query string. * * @param Query|string|array $query Query string value to set. Can * be a string that will be parsed into a Query object, an array * of key value pairs, or a Query object. * @param bool $rawString Set to true when providing a raw query string. * * @throws \InvalidArgumentException */ public function setQuery($query, $rawString = false) { if ($query instanceof Query) { $this->query = $query; } elseif (is_string($query)) { if (!$rawString) { $this->query = Query::fromString($query); } else { // Ensure the query does not have illegal characters. $this->query = preg_replace_callback(self::$queryPattern, array(__CLASS__, 'encodeMatch'), $query); } } elseif (is_array($query)) { $this->query = new Query($query); } else { throw new \InvalidArgumentException('Query must be a Query, ' . 'array, or string. Got ' . Core::describeType($query)); } }
public function testParsesLastResponseOnly() { $response1 = ['status' => 301, 'headers' => ['Content-Length' => ['0'], 'Location' => ['/foo']]]; $response2 = ['status' => 200, 'headers' => ['Content-Length' => ['0'], 'Foo' => ['bar']]]; Server::flush(); Server::enqueue([$response1, $response2]); $a = new CurlMultiHandler(); $response = $a(['http_method' => 'GET', 'headers' => ['Host' => [Server::$host]], 'client' => ['curl' => [CURLOPT_FOLLOWLOCATION => true]]])->wait(); $this->assertEquals(1, $response['transfer_stats']['redirect_count']); $this->assertEquals('http://127.0.0.1:8125/foo', $response['effective_url']); $this->assertEquals(['bar'], $response['headers']['Foo']); $this->assertEquals(200, $response['status']); $this->assertFalse(Core::hasHeader($response, 'Location')); }
/** * Adds the next request to pool and tracks what requests need to be * dereferenced when completing the pool. */ private function addNextRequest() { add_next: if ($this->isRealized || !$this->iter || !$this->iter->valid()) { return false; } $request = $this->iter->current(); $this->iter->next(); if (!$request instanceof RequestInterface) { throw new \InvalidArgumentException(sprintf('All requests in the provided iterator must implement ' . 'RequestInterface. Found %s', Core::describeType($request))); } // Be sure to use "lazy" futures, meaning they do not send right away. $request->getConfig()->set('future', 'lazy'); $hash = spl_object_hash($request); $this->attachListeners($request, $this->eventListeners); $request->getEmitter()->on('before', [$this, '_trackRetries'], RequestEvents::EARLY); $response = $this->client->send($request); $this->waitQueue[$hash] = $response; $promise = $response->promise(); // Don't recursively call itself for completed or rejected responses. if ($promise instanceof FulfilledPromise || $promise instanceof RejectedPromise) { try { $this->finishResponse($request, $response->wait(), $hash); } catch (\Exception $e) { $this->finishResponse($request, $e, $hash); } goto add_next; } // Use this function for both resolution and rejection. $thenFn = function ($value) use($request, $hash) { $this->finishResponse($request, $value, $hash); if (!$request->getConfig()->get('_pool_retries')) { $this->addNextRequests(); } }; $promise->then($thenFn, $thenFn); return true; }
private function checkAssociativeArray($value) { if (!is_array($value) || isset($value[0])) { $this->addError('must be an associative array. Found ' . Core::describeType($value)); return false; } return true; }