/**
  * @param string $verb
  * @param string $content
  * @param string $contentType
  * @param bool $contentFromFile - if true, then $content is assumed to be a file path
  * @return array - request response
  */
 protected function invoke($verb, $content = null, $contentType = null, $contentFromFile = false)
 {
     // create the request info
     $request = ['verb' => $verb, 'uri' => $this->getUri(), 'start' => 0, 'end' => 0, 'headers' => $this->headers];
     // explicitly set content length for empty bodies
     if (is_null($content) || $content === false || is_string($content) && strlen($content) == 0) {
         self::setMultiValueArray($request['headers'], self::HEADER_CONTENT_LENGTH, 0);
     }
     // set the content type if provided
     if (!is_null($contentType)) {
         self::setMultiValueArray($request['headers'], self::HEADER_CONTENT_TYPE, $contentType);
     }
     $this->invokeApplyCredentials($request['headers']);
     // if MockPlug returns a response, curl is not needed
     if (MockPlug::$registered) {
         $Response = MockPlug::getResponse(MockRequest::newMockRequest($verb, $request['uri'], $request['headers'], $content));
         if ($Response !== null) {
             $response = ['verb' => $verb, 'body' => $Response->body, 'headers' => $Response->headers, 'status' => $Response->status, 'errno' => '', 'error' => ''];
             if (isset($Response->headers[self::HEADER_CONTENT_TYPE])) {
                 $response['type'] = $Response->headers[self::HEADER_CONTENT_TYPE];
             }
             $request['headers'] = self::flattenPlugHeaders($request['headers']);
             return $this->invokeComplete($request, $response);
         }
     }
     // normal plug request
     $curl = curl_init();
     curl_setopt($curl, CURLOPT_URL, $request['uri']);
     curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
     curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);
     curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
     curl_setopt($curl, CURLOPT_MAXREDIRS, 10);
     curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $verb);
     curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
     // custom behavior based on the request type
     switch ($verb) {
         case self::VERB_PUT:
             if ($contentFromFile && is_file($content)) {
                 // read in content from file
                 curl_setopt($curl, CURLOPT_PUT, true);
                 curl_setopt($curl, CURLOPT_INFILE, fopen($content, 'r'));
                 curl_setopt($curl, CURLOPT_INFILESIZE, filesize($content));
             }
             break;
         case self::VERB_POST:
             /**
              * The full data to post in a HTTP "POST" operation. To post a file, prepend a filename with @ and use the full path.
              * This can either be passed as a urlencoded string like 'para1=val1&para2=val2&...' or as an array with the field name as
              * key and field data as value. If value is an array, the Content-Type header will be set to multipart/form-data.
              */
             if ($contentFromFile && is_file($content)) {
                 curl_setopt($curl, CURLOPT_POST, true);
                 $postFields = ['file' => '@' . $content];
                 curl_setopt($curl, CURLOPT_POSTFIELDS, $postFields);
             } else {
                 curl_setopt($curl, CURLOPT_POSTFIELDS, $content);
             }
             break;
         default:
     }
     // add the request headers
     if (!empty($request['headers'])) {
         // flatten headers
         $request['headers'] = self::flattenPlugHeaders($request['headers']);
         curl_setopt($curl, CURLOPT_HTTPHEADER, $request['headers']);
     }
     // retrieve the response headers
     curl_setopt($curl, CURLOPT_HEADER, true);
     // execute request
     $request['start'] = $this->getTime();
     $httpMessage = curl_exec($curl);
     $request['end'] = $this->getTime();
     // create the response info
     $response = ['headers' => [], 'status' => curl_getinfo($curl, CURLINFO_HTTP_CODE), 'type' => curl_getinfo($curl, CURLINFO_CONTENT_TYPE), 'errno' => curl_errno($curl), 'error' => curl_error($curl)];
     curl_close($curl);
     // header parsing
     // make sure ther response is not empty before trying to parse
     // also make sure there isn't a curl error
     if ($response['status'] != 0 && $response['errno'] == 0) {
         // split response into header and response body
         do {
             list($headers, $httpMessage) = explode("\r\n\r\n", $httpMessage, 2);
             $headers = explode("\r\n", $headers);
             // First line of headers is the HTTP response code, remove it
             $httpStatus = array_shift($headers);
             // check if there is another header chunk to parse
         } while ($httpStatus == 'HTTP/1.1 100 Continue');
         // set the response body
         $response['body'] =& $httpMessage;
         // put the rest of the headers in an array
         foreach ($headers as $headerLine) {
             list($header, $value) = explode(': ', $headerLine, 2);
             // allow for multiple header values
             self::setMultiValueArray($response['headers'], $header, trim($value), true);
         }
     }
     return $this->invokeComplete($request, $response);
 }