| Total Complexity | 44 | 
| Total Lines | 288 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
Complex classes like TraceMiddleware often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use TraceMiddleware, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 13 | class TraceMiddleware | ||
| 14 | { | ||
| 15 | private $prevOutput; | ||
| 16 | private $prevInput; | ||
| 17 | private $config; | ||
| 18 | |||
| 19 | private static $authHeaders = [ | ||
| 20 | 'X-Amz-Security-Token' => '[TOKEN]', | ||
| 21 | ]; | ||
| 22 | |||
| 23 | private static $authStrings = [ | ||
| 24 | // S3Signature | ||
| 25 |         '/AWSAccessKeyId=[A-Z0-9]{20}&/i' => 'AWSAccessKeyId=[KEY]&', | ||
| 26 | // SignatureV4 Signature and S3Signature | ||
| 27 | '/Signature=.+/i' => 'Signature=[SIGNATURE]', | ||
| 28 | // SignatureV4 access key ID | ||
| 29 |         '/Credential=[A-Z0-9]{20}\//i' => 'Credential=[KEY]/', | ||
| 30 | // S3 signatures | ||
| 31 |         '/AWS [A-Z0-9]{20}:.+/' => 'AWS AKI[KEY]:[SIGNATURE]', | ||
| 32 | // STS Presigned URLs | ||
| 33 | '/X-Amz-Security-Token=[^&]+/i' => 'X-Amz-Security-Token=[TOKEN]', | ||
| 34 | ]; | ||
| 35 | |||
| 36 | /** | ||
| 37 | * Configuration array can contain the following key value pairs. | ||
| 38 | * | ||
| 39 | * - logfn: (callable) Function that is invoked with log messages. By | ||
| 40 | * default, PHP's "echo" function will be utilized. | ||
| 41 | * - stream_size: (int) When the size of a stream is greater than this | ||
| 42 | * number, the stream data will not be logged. Set to "0" to not log any | ||
| 43 | * stream data. | ||
| 44 | * - scrub_auth: (bool) Set to false to disable the scrubbing of auth data | ||
| 45 | * from the logged messages. | ||
| 46 | * - http: (bool) Set to false to disable the "debug" feature of lower | ||
| 47 | * level HTTP adapters (e.g., verbose curl output). | ||
| 48 | * - auth_strings: (array) A mapping of authentication string regular | ||
| 49 | * expressions to scrubbed strings. These mappings are passed directly to | ||
| 50 | * preg_replace (e.g., preg_replace($key, $value, $debugOutput) if | ||
| 51 | * "scrub_auth" is set to true. | ||
| 52 | * - auth_headers: (array) A mapping of header names known to contain | ||
| 53 | * sensitive data to what the scrubbed value should be. The value of any | ||
| 54 | * headers contained in this array will be replaced with the if | ||
| 55 | * "scrub_auth" is set to true. | ||
| 56 | */ | ||
| 57 | public function __construct(array $config = []) | ||
| 58 |     { | ||
| 59 | $this->config = $config + [ | ||
| 60 |             'logfn'        => function ($value) { echo $value; }, | ||
| 61 | 'stream_size' => 524288, | ||
| 62 | 'scrub_auth' => true, | ||
| 63 | 'http' => true, | ||
| 64 | 'auth_strings' => [], | ||
| 65 | 'auth_headers' => [], | ||
| 66 | ]; | ||
| 67 | |||
| 68 | $this->config['auth_strings'] += self::$authStrings; | ||
| 69 | $this->config['auth_headers'] += self::$authHeaders; | ||
| 70 | } | ||
| 71 | |||
| 72 | public function __invoke($step, $name) | ||
| 73 |     { | ||
| 74 | $this->prevOutput = $this->prevInput = []; | ||
| 75 | |||
| 76 |         return function (callable $next) use ($step, $name) { | ||
| 77 | return function ( | ||
| 78 | CommandInterface $command, | ||
| 79 | RequestInterface $request = null | ||
| 80 |             ) use ($next, $step, $name) { | ||
| 81 | $this->createHttpDebug($command); | ||
| 82 | $start = microtime(true); | ||
| 83 | $this->stepInput([ | ||
| 84 | 'step' => $step, | ||
| 85 | 'name' => $name, | ||
| 86 | 'request' => $this->requestArray($request), | ||
| 87 | 'command' => $this->commandArray($command) | ||
| 88 | ]); | ||
| 89 | |||
| 90 | return $next($command, $request)->then( | ||
| 91 |                     function ($value) use ($step, $name, $command, $start) { | ||
| 92 | $this->flushHttpDebug($command); | ||
| 93 | $this->stepOutput($start, [ | ||
| 94 | 'step' => $step, | ||
| 95 | 'name' => $name, | ||
| 96 | 'result' => $this->resultArray($value), | ||
| 97 | 'error' => null | ||
| 98 | ]); | ||
| 99 | return $value; | ||
| 100 | }, | ||
| 101 |                     function ($reason) use ($step, $name, $start, $command) { | ||
| 102 | $this->flushHttpDebug($command); | ||
| 103 | $this->stepOutput($start, [ | ||
| 104 | 'step' => $step, | ||
| 105 | 'name' => $name, | ||
| 106 | 'result' => null, | ||
| 107 | 'error' => $this->exceptionArray($reason) | ||
| 108 | ]); | ||
| 109 | return new RejectedPromise($reason); | ||
| 110 | } | ||
| 111 | ); | ||
| 112 | }; | ||
| 113 | }; | ||
| 114 | } | ||
| 115 | |||
| 116 | private function stepInput($entry) | ||
| 117 |     { | ||
| 118 | static $keys = ['command', 'request']; | ||
| 119 | $this->compareStep($this->prevInput, $entry, '-> Entering', $keys); | ||
| 120 |         $this->write("\n"); | ||
| 121 | $this->prevInput = $entry; | ||
| 122 | } | ||
| 123 | |||
| 124 | private function stepOutput($start, $entry) | ||
| 125 |     { | ||
| 126 | static $keys = ['result', 'error']; | ||
| 127 | $this->compareStep($this->prevOutput, $entry, '<- Leaving', $keys); | ||
| 128 | $totalTime = microtime(true) - $start; | ||
| 129 |         $this->write("  Inclusive step time: " . $totalTime . "\n\n"); | ||
| 130 | $this->prevOutput = $entry; | ||
| 131 | } | ||
| 132 | |||
| 133 | private function compareStep(array $a, array $b, $title, array $keys) | ||
| 134 |     { | ||
| 135 | $changes = []; | ||
| 136 |         foreach ($keys as $key) { | ||
| 137 | $av = isset($a[$key]) ? $a[$key] : null; | ||
| 138 | $bv = isset($b[$key]) ? $b[$key] : null; | ||
| 139 | $this->compareArray($av, $bv, $key, $changes); | ||
| 140 | } | ||
| 141 |         $str = "\n{$title} step {$b['step']}, name '{$b['name']}'"; | ||
| 142 |         $str .= "\n" . str_repeat('-', strlen($str) - 1) . "\n\n  "; | ||
| 143 | $str .= $changes | ||
| 144 |             ? implode("\n  ", str_replace("\n", "\n  ", $changes)) | ||
| 145 | : 'no changes'; | ||
| 146 | $this->write($str . "\n"); | ||
| 147 | } | ||
| 148 | |||
| 149 | private function commandArray(CommandInterface $cmd) | ||
| 150 |     { | ||
| 151 | return [ | ||
| 152 | 'instance' => spl_object_hash($cmd), | ||
| 153 | 'name' => $cmd->getName(), | ||
| 154 | 'params' => $cmd->toArray() | ||
| 155 | ]; | ||
| 156 | } | ||
| 157 | |||
| 158 | private function requestArray(RequestInterface $request = null) | ||
| 159 |     { | ||
| 160 | return !$request ? [] : array_filter([ | ||
| 161 | 'instance' => spl_object_hash($request), | ||
| 162 | 'method' => $request->getMethod(), | ||
| 163 | 'headers' => $this->redactHeaders($request->getHeaders()), | ||
| 164 | 'body' => $this->streamStr($request->getBody()), | ||
| 165 | 'scheme' => $request->getUri()->getScheme(), | ||
| 166 | 'port' => $request->getUri()->getPort(), | ||
| 167 | 'path' => $request->getUri()->getPath(), | ||
| 168 | 'query' => $request->getUri()->getQuery(), | ||
| 169 | ]); | ||
| 170 | } | ||
| 171 | |||
| 172 | private function responseArray(ResponseInterface $response = null) | ||
| 173 |     { | ||
| 174 | return !$response ? [] : [ | ||
| 175 | 'instance' => spl_object_hash($response), | ||
| 176 | 'statusCode' => $response->getStatusCode(), | ||
| 177 | 'headers' => $this->redactHeaders($response->getHeaders()), | ||
| 178 | 'body' => $this->streamStr($response->getBody()) | ||
| 179 | ]; | ||
| 180 | } | ||
| 181 | |||
| 182 | private function resultArray($value) | ||
| 183 |     { | ||
| 184 | return $value instanceof ResultInterface | ||
| 185 | ? [ | ||
| 186 | 'instance' => spl_object_hash($value), | ||
| 187 | 'data' => $value->toArray() | ||
| 188 | ] : $value; | ||
| 189 | } | ||
| 190 | |||
| 191 | private function exceptionArray($e) | ||
| 192 |     { | ||
| 193 |         if (!($e instanceof \Exception)) { | ||
| 194 | return $e; | ||
| 195 | } | ||
| 196 | |||
| 197 | $result = [ | ||
| 198 | 'instance' => spl_object_hash($e), | ||
| 199 | 'class' => get_class($e), | ||
| 200 | 'message' => $e->getMessage(), | ||
| 201 | 'file' => $e->getFile(), | ||
| 202 | 'line' => $e->getLine(), | ||
| 203 | 'trace' => $e->getTraceAsString(), | ||
| 204 | ]; | ||
| 205 | |||
| 206 |         if ($e instanceof AwsException) { | ||
| 207 | $result += [ | ||
| 208 | 'type' => $e->getAwsErrorType(), | ||
| 209 | 'code' => $e->getAwsErrorCode(), | ||
| 210 | 'requestId' => $e->getAwsRequestId(), | ||
| 211 | 'statusCode' => $e->getStatusCode(), | ||
| 212 | 'result' => $this->resultArray($e->getResult()), | ||
| 213 | 'request' => $this->requestArray($e->getRequest()), | ||
| 214 | 'response' => $this->responseArray($e->getResponse()), | ||
| 215 | ]; | ||
| 216 | } | ||
| 217 | |||
| 218 | return $result; | ||
| 219 | } | ||
| 220 | |||
| 221 | private function compareArray($a, $b, $path, array &$diff) | ||
| 222 |     { | ||
| 223 |         if ($a === $b) { | ||
| 224 | return; | ||
| 225 |         } elseif (is_array($a)) { | ||
| 226 | $b = (array) $b; | ||
| 227 | $keys = array_unique(array_merge(array_keys($a), array_keys($b))); | ||
| 228 |             foreach ($keys as $k) { | ||
| 229 |                 if (!array_key_exists($k, $a)) { | ||
| 230 |                     $this->compareArray(null, $b[$k], "{$path}.{$k}", $diff); | ||
| 231 |                 } elseif (!array_key_exists($k, $b)) { | ||
| 232 |                     $this->compareArray($a[$k], null, "{$path}.{$k}", $diff); | ||
| 233 |                 } else { | ||
| 234 |                     $this->compareArray($a[$k], $b[$k], "{$path}.{$k}", $diff); | ||
| 235 | } | ||
| 236 | } | ||
| 237 |         } elseif ($a !== null && $b === null) { | ||
| 238 |             $diff[] = "{$path} was unset"; | ||
| 239 |         } elseif ($a === null && $b !== null) { | ||
| 240 |             $diff[] = sprintf("%s was set to %s", $path, $this->str($b)); | ||
| 241 |         } else { | ||
| 242 |             $diff[] = sprintf("%s changed from %s to %s", $path, $this->str($a), $this->str($b)); | ||
| 243 | } | ||
| 244 | } | ||
| 245 | |||
| 246 | private function str($value) | ||
| 247 |     { | ||
| 248 |         if (is_scalar($value)) { | ||
| 249 | return (string) $value; | ||
| 250 |         } elseif ($value instanceof \Exception) { | ||
| 251 | $value = $this->exceptionArray($value); | ||
| 252 | } | ||
| 253 | |||
| 254 | ob_start(); | ||
| 255 | var_dump($value); | ||
|  | |||
| 256 | return ob_get_clean(); | ||
| 257 | } | ||
| 258 | |||
| 259 | private function streamStr(StreamInterface $body) | ||
| 264 | } | ||
| 265 | |||
| 266 | private function createHttpDebug(CommandInterface $command) | ||
| 267 |     { | ||
| 268 |         if ($this->config['http'] && !isset($command['@http']['debug'])) { | ||
| 269 |             $command['@http']['debug'] = fopen('php://temp', 'w+'); | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | private function flushHttpDebug(CommandInterface $command) | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | private function write($value) | ||
| 284 |     { | ||
| 285 |         if ($this->config['scrub_auth']) { | ||
| 286 |             foreach ($this->config['auth_strings'] as $pattern => $replacement) { | ||
| 287 | $value = preg_replace($pattern, $replacement, $value); | ||
| 288 | } | ||
| 289 | } | ||
| 290 | |||
| 291 | call_user_func($this->config['logfn'], $value); | ||
| 292 | } | ||
| 293 | |||
| 294 | private function redactHeaders(array $headers) | ||
| 301 | } | ||
| 302 | } | ||
| 303 |