| Total Complexity | 52 |
| Total Lines | 438 |
| Duplicated Lines | 0 % |
| Changes | 20 | ||
| Bugs | 1 | Features | 0 |
Complex classes like OAuthProvider 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 OAuthProvider, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 45 | abstract class OAuthProvider implements OAuthInterface{ |
||
| 46 | use LoggerAwareTrait; |
||
| 47 | |||
| 48 | protected const ALLOWED_PROPERTIES = [ |
||
| 49 | 'apiDocs', 'apiURL', 'applicationURL', 'endpoints', 'serviceName', 'userRevokeURL' |
||
| 50 | ]; |
||
| 51 | |||
| 52 | /** |
||
| 53 | * the http client instance |
||
| 54 | */ |
||
| 55 | protected ClientInterface $http; |
||
| 56 | |||
| 57 | /** |
||
| 58 | * the token storage instance |
||
| 59 | */ |
||
| 60 | protected OAuthStorageInterface $storage; |
||
| 61 | |||
| 62 | /** |
||
| 63 | * the options instance |
||
| 64 | * |
||
| 65 | * @var \chillerlan\OAuth\OAuthOptions|\chillerlan\Settings\SettingsContainerInterface |
||
| 66 | */ |
||
| 67 | protected SettingsContainerInterface $options; |
||
| 68 | |||
| 69 | /** |
||
| 70 | * the API endpoints (optional) (magic) |
||
| 71 | */ |
||
| 72 | protected ?EndpointMapInterface $endpoints = null; |
||
| 73 | |||
| 74 | /** |
||
| 75 | * an optional PSR-17 request factory |
||
| 76 | */ |
||
| 77 | protected RequestFactoryInterface $requestFactory; |
||
| 78 | |||
| 79 | /** |
||
| 80 | * an optional PSR-17 stream factory |
||
| 81 | */ |
||
| 82 | protected StreamFactoryInterface $streamFactory; |
||
| 83 | |||
| 84 | /** |
||
| 85 | * an optional PSR-17 URI factory |
||
| 86 | */ |
||
| 87 | protected UriFactoryInterface $uriFactory; |
||
| 88 | |||
| 89 | /** |
||
| 90 | * the name of the provider (class) (magic) |
||
| 91 | */ |
||
| 92 | protected ?string $serviceName = null; |
||
| 93 | |||
| 94 | /** |
||
| 95 | * the authentication URL |
||
| 96 | */ |
||
| 97 | protected string $authURL; |
||
| 98 | |||
| 99 | /** |
||
| 100 | * an optional link to the provider's API docs (magic) |
||
| 101 | */ |
||
| 102 | protected ?string $apiDocs = null; |
||
| 103 | |||
| 104 | /** |
||
| 105 | * the API base URL (magic) |
||
| 106 | */ |
||
| 107 | protected ?string $apiURL = null; |
||
| 108 | |||
| 109 | /** |
||
| 110 | * an optional URL to the provider's credential registration/application page (magic) |
||
| 111 | */ |
||
| 112 | protected ?string $applicationURL = null; |
||
| 113 | |||
| 114 | /** |
||
| 115 | * an optional link to the page where a user can revoke access tokens (magic) |
||
| 116 | */ |
||
| 117 | protected ?string $userRevokeURL = null; |
||
| 118 | |||
| 119 | /** |
||
| 120 | * an optional URL for application side token revocation |
||
| 121 | */ |
||
| 122 | protected ?string $revokeURL = null; |
||
| 123 | |||
| 124 | /** |
||
| 125 | * the provider's access token exchange URL |
||
| 126 | */ |
||
| 127 | protected string $accessTokenURL; |
||
| 128 | |||
| 129 | /** |
||
| 130 | * an optional EndpointMapInterface FQCN |
||
| 131 | */ |
||
| 132 | protected ?string $endpointMap = null; |
||
| 133 | |||
| 134 | /** |
||
| 135 | * additional headers to use during authentication |
||
| 136 | */ |
||
| 137 | protected array $authHeaders = []; |
||
| 138 | |||
| 139 | /** |
||
| 140 | * additional headers to use during API access |
||
| 141 | */ |
||
| 142 | protected array $apiHeaders = []; |
||
| 143 | |||
| 144 | /** |
||
| 145 | * OAuthProvider constructor. |
||
| 146 | * |
||
| 147 | * @param \Psr\Http\Client\ClientInterface $http |
||
| 148 | * @param \chillerlan\OAuth\Storage\OAuthStorageInterface $storage |
||
| 149 | * @param \chillerlan\Settings\SettingsContainerInterface $options |
||
| 150 | * @param \Psr\Log\LoggerInterface|null $logger |
||
| 151 | * |
||
| 152 | * @throws \chillerlan\OAuth\MagicAPI\ApiClientException |
||
| 153 | */ |
||
| 154 | public function __construct( |
||
| 155 | ClientInterface $http, |
||
| 156 | OAuthStorageInterface $storage, |
||
| 157 | SettingsContainerInterface $options, |
||
| 158 | LoggerInterface $logger = null |
||
| 159 | ){ |
||
| 160 | $this->http = $http; |
||
| 161 | $this->storage = $storage; |
||
| 162 | $this->options = $options; |
||
| 163 | $this->logger = $logger ?? new NullLogger; |
||
| 164 | |||
| 165 | // i hate this, but i also hate adding 3 more params to the constructor |
||
| 166 | // no i won't use a DI container for this. don't @ me |
||
| 167 | $this->requestFactory = new RequestFactory; |
||
| 168 | $this->streamFactory = new StreamFactory; |
||
| 169 | $this->uriFactory = new UriFactory; |
||
| 170 | |||
| 171 | $this->serviceName = (new ReflectionClass($this))->getShortName(); |
||
| 172 | |||
| 173 | if(!empty($this->endpointMap) && class_exists($this->endpointMap)){ |
||
| 174 | $this->endpoints = new $this->endpointMap; |
||
| 175 | |||
| 176 | if(!$this->endpoints instanceof EndpointMapInterface){ |
||
| 177 | throw new ApiClientException('invalid endpoint map'); // @codeCoverageIgnore |
||
| 178 | } |
||
| 179 | |||
| 180 | } |
||
| 181 | |||
| 182 | } |
||
| 183 | |||
| 184 | /** |
||
| 185 | * Magic getter for the properties specified in self::ALLOWED_PROPERTIES |
||
| 186 | * |
||
| 187 | * @param string $name |
||
| 188 | * |
||
| 189 | * @return mixed|null |
||
| 190 | */ |
||
| 191 | public function __get(string $name){ |
||
| 192 | |||
| 193 | if(in_array($name, $this::ALLOWED_PROPERTIES, true)){ |
||
| 194 | return $this->{$name}; |
||
| 195 | } |
||
| 196 | |||
| 197 | return null; |
||
| 198 | } |
||
| 199 | |||
| 200 | /** |
||
| 201 | * @inheritDoc |
||
| 202 | * @codeCoverageIgnore |
||
| 203 | */ |
||
| 204 | public function setStorage(OAuthStorageInterface $storage):OAuthInterface{ |
||
| 205 | $this->storage = $storage; |
||
| 206 | |||
| 207 | return $this; |
||
| 208 | } |
||
| 209 | |||
| 210 | /** |
||
| 211 | * @inheritDoc |
||
| 212 | * @codeCoverageIgnore |
||
| 213 | */ |
||
| 214 | public function setRequestFactory(RequestFactoryInterface $requestFactory):OAuthInterface{ |
||
| 215 | $this->requestFactory = $requestFactory; |
||
| 216 | |||
| 217 | return $this; |
||
| 218 | } |
||
| 219 | |||
| 220 | /** |
||
| 221 | * @inheritDoc |
||
| 222 | * @codeCoverageIgnore |
||
| 223 | */ |
||
| 224 | public function setStreamFactory(StreamFactoryInterface $streamFactory):OAuthInterface{ |
||
| 225 | $this->streamFactory = $streamFactory; |
||
| 226 | |||
| 227 | return $this; |
||
| 228 | } |
||
| 229 | |||
| 230 | /** |
||
| 231 | * @inheritDoc |
||
| 232 | * @codeCoverageIgnore |
||
| 233 | */ |
||
| 234 | public function setUriFactory(UriFactoryInterface $uriFactory):OAuthInterface{ |
||
| 235 | $this->uriFactory = $uriFactory; |
||
| 236 | |||
| 237 | return $this; |
||
| 238 | } |
||
| 239 | |||
| 240 | /** |
||
| 241 | * Magic API endpoint access. ugly, isn't it? |
||
| 242 | * |
||
| 243 | * @param string $endpointName |
||
| 244 | * @param array $arguments |
||
| 245 | * |
||
| 246 | * @return \Psr\Http\Message\ResponseInterface |
||
| 247 | * @throws \chillerlan\OAuth\MagicAPI\ApiClientException |
||
| 248 | */ |
||
| 249 | public function __call(string $endpointName, array $arguments):ResponseInterface{ |
||
| 250 | |||
| 251 | if(!$this->endpoints instanceof EndpointMap){ |
||
| 252 | throw new ApiClientException('MagicAPI not available'); // @codeCoverageIgnore |
||
| 253 | } |
||
| 254 | |||
| 255 | if(!isset($this->endpoints->{$endpointName})){ |
||
| 256 | throw new ApiClientException('endpoint not found: "'.$endpointName.'"'); |
||
| 257 | } |
||
| 258 | |||
| 259 | // metadata for the current endpoint |
||
| 260 | $endpointMeta = $this->endpoints->{$endpointName}; |
||
| 261 | $path = $this->endpoints->API_BASE.($endpointMeta['path'] ?? ''); |
||
|
|
|||
| 262 | $method = $endpointMeta['method'] ?? 'GET'; |
||
| 263 | $path_elements = $endpointMeta['path_elements'] ?? []; |
||
| 264 | $query_params = $endpointMeta['query'] ?? []; |
||
| 265 | $headers = $endpointMeta['headers'] ?? []; |
||
| 266 | // the body value of the metadata is only informational |
||
| 267 | $has_body = isset($endpointMeta['body']) && !empty($endpointMeta['body']); |
||
| 268 | |||
| 269 | $params = null; |
||
| 270 | $body = null; |
||
| 271 | |||
| 272 | $path_element_count = count($path_elements); |
||
| 273 | $query_param_count = count($query_params); |
||
| 274 | |||
| 275 | if($path_element_count > 0){ |
||
| 276 | $path = $this->parsePathElements($path, $path_elements, $path_element_count, $arguments); |
||
| 277 | } |
||
| 278 | |||
| 279 | if($query_param_count > 0){ |
||
| 280 | // $params is the first argument after path segments |
||
| 281 | $params = $arguments[$path_element_count] ?? null; |
||
| 282 | |||
| 283 | if(is_array($params)){ |
||
| 284 | $params = $this->cleanQueryParams($this->removeUnlistedParams($params, $query_params)); |
||
| 285 | } |
||
| 286 | } |
||
| 287 | |||
| 288 | if(in_array($method, ['POST', 'PATCH', 'PUT', 'DELETE']) && $has_body){ |
||
| 289 | // if no query params are present, $body is the first argument after any path segments |
||
| 290 | $argPos = $query_param_count > 0 ? 1 : 0; |
||
| 291 | $body = $arguments[$path_element_count + $argPos] ?? null; |
||
| 292 | |||
| 293 | if(is_array($body)){ |
||
| 294 | $body = $this->cleanBodyParams($body); |
||
| 295 | } |
||
| 296 | } |
||
| 297 | |||
| 298 | $this->logger->debug('OAuthProvider::__call() -> '.$this->serviceName.'::'.$endpointName.'()', [ |
||
| 299 | '$endpoint' => $path, '$params' => $params, '$method' => $method, '$body' => $body, '$headers' => $headers, |
||
| 300 | ]); |
||
| 301 | |||
| 302 | return $this->request($path, $params, $method, $body, $headers); |
||
| 303 | } |
||
| 304 | |||
| 305 | /** |
||
| 306 | * Checks the given path elements and returns the given path with placeholders replaced |
||
| 307 | * |
||
| 308 | * @throws \chillerlan\OAuth\MagicAPI\ApiClientException |
||
| 309 | */ |
||
| 310 | protected function parsePathElements(string $path, array $path_elements, int $path_element_count, array $arguments):string{ |
||
| 311 | // we don't know if all of the given arguments are path elements... |
||
| 312 | $urlparams = array_slice($arguments, 0, $path_element_count); |
||
| 313 | |||
| 314 | if(count($urlparams) !== $path_element_count){ |
||
| 315 | throw new APIClientException('too few URL params, required: '.implode(', ', $path_elements)); |
||
| 316 | } |
||
| 317 | |||
| 318 | foreach($urlparams as $i => $param){ |
||
| 319 | // ...but we do know that the arguments after the path elements are usually array or null |
||
| 320 | if(!is_scalar($param)){ |
||
| 321 | $msg = 'invalid path element value for "%s": %s'; |
||
| 322 | |||
| 323 | throw new APIClientException(sprintf($msg, $path_elements[$i], var_export($param, true))); |
||
| 324 | } |
||
| 325 | } |
||
| 326 | |||
| 327 | return sprintf($path, ...$urlparams); |
||
| 328 | } |
||
| 329 | |||
| 330 | /** |
||
| 331 | * Checks an array against an allowlist and removes any parameter that is not allowed |
||
| 332 | */ |
||
| 333 | protected function removeUnlistedParams(array $params, array $allowed):array{ |
||
| 346 | } |
||
| 347 | |||
| 348 | /** |
||
| 349 | * Cleans an array of query parameters |
||
| 350 | */ |
||
| 351 | protected function cleanQueryParams(iterable $params):array{ |
||
| 353 | } |
||
| 354 | |||
| 355 | /** |
||
| 356 | * Cleans an array of body parameters |
||
| 357 | */ |
||
| 358 | protected function cleanBodyParams(iterable $params):array{ |
||
| 360 | } |
||
| 361 | |||
| 362 | /** |
||
| 363 | * Merges a set of parameters into the given querystring and returns the result querystring |
||
| 364 | */ |
||
| 365 | protected function mergeQuery(string $uri, array $query):string{ |
||
| 366 | return Query::merge($uri, $query); |
||
| 367 | } |
||
| 368 | |||
| 369 | /** |
||
| 370 | * Builds a query string from the given parameters |
||
| 371 | */ |
||
| 372 | protected function buildQuery(array $params, int $encoding = null, string $delimiter = null, string $enclosure = null):string{ |
||
| 374 | } |
||
| 375 | |||
| 376 | /** |
||
| 377 | * Parses the given querystring into an associative array |
||
| 378 | */ |
||
| 379 | protected function parseQuery(string $querystring, int $urlEncoding = null):array{ |
||
| 380 | return Query::parse($querystring, $urlEncoding); |
||
| 381 | } |
||
| 382 | |||
| 383 | /** |
||
| 384 | * @inheritDoc |
||
| 385 | */ |
||
| 386 | public function request( |
||
| 426 | } |
||
| 427 | |||
| 428 | /** |
||
| 429 | * Determine the request target from the given URI (path segment or URL) with respect to $apiURL, |
||
| 430 | * anything except host and path will be ignored, scheme will always be set to "https". |
||
| 431 | * Throws if the given path is invalid or if the host of a given URL does not match $apiURL. |
||
| 432 | * |
||
| 433 | * @see \chillerlan\OAuth\Core\OAuthInterface::request() |
||
| 434 | * |
||
| 435 | * @throws \chillerlan\OAuth\Core\ProviderException |
||
| 436 | */ |
||
| 437 | protected function getRequestTarget(string $uri):string{ |
||
| 459 | } |
||
| 460 | |||
| 461 | /** |
||
| 462 | * @inheritDoc |
||
| 463 | */ |
||
| 464 | public function sendRequest(RequestInterface $request):ResponseInterface{ |
||
| 486 |