Insphpect

This tool is currently proof-of-concept. Your feedback and evaluation is valuable in helping to improve it and ensure its reports are meaninful.

Please click here to complete a short survey to tell us what you think. It should take less than 5 minutes and help further this research project!

GuzzleHttp\Client

Detected issues

Issue Method Line number
Annotations N/A 118

Code

Click highlighted lines for details

<?phpnamespace GuzzleHttp;use GuzzleHttp\Cookie\CookieJar;use GuzzleHttp\Exception\GuzzleException;use GuzzleHttp\Exception\InvalidArgumentException;use GuzzleHttp\Promise\PromiseInterface;use Psr\Http\Message\RequestInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\UriInterface;class Client implements ClientInterface, \Psr\Http\Client\ClientInterface{    use ClientTrait;    /** @var array Default request options */    private $config;    /**     * Clients accept an array of constructor parameters.     *     * Here's an example of creating a client using a base_uri and an array of     * default request options to apply to each request:     *     *     $client = new Client([     *         'base_uri'        => 'http://www.foo.com/1.0/',     *         'timeout'         => 0,     *         'allow_redirects' => false,     *         'proxy'           => '192.168.16.1:10'     *     ]);     *     * Client configuration settings include the following options:     *     * - handler: (callable) Function that transfers HTTP requests over the     *   wire. The function is called with a Psr7\Http\Message\RequestInterface     *   and array of transfer options, and must return a     *   GuzzleHttp\Promise\PromiseInterface that is fulfilled with a     *   Psr7\Http\Message\ResponseInterface on success. "handler" is a     *   constructor only option that cannot be overridden in per/request     *   options. If no handler is provided, a default handler will be created     *   that enables all of the request options below by attaching all of the     *   default middleware to the handler.     * - base_uri: (string|UriInterface) Base URI of the client that is merged     *   into relative URIs. Can be a string or instance of UriInterface.     * - **: any request option     *     * @param array $config Client configuration settings.     *     * @see \GuzzleHttp\RequestOptions for a list of available request options.     */    public function __construct(array $config = [])    {        if (!isset($config['handler'])) {            $config['handler'] = HandlerStack::create();        } elseif (!\is_callable($config['handler'])) {            throw new InvalidArgumentException('handler must be a callable');        }        // Convert the base_uri to a UriInterface        if (isset($config['base_uri'])) {            $config['base_uri'] = Psr7\uri_for($config['base_uri']);        }        $this->configureDefaults($config);    }    /**     * @param string $method     * @param array  $args     *     * @return PromiseInterface|ResponseInterface     */    public function __call($method, $args)    {        if (\count($args) < 1) {            throw new InvalidArgumentException('Magic request methods require a URI and optional options array');        }        $uri = $args[0];        $opts = isset($args[1]) ? $args[1] : [];        return \substr($method, -5) === 'Async'            ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)            : $this->request($method, $uri, $opts);    }    /**     * Asynchronously send an HTTP request.     *     * @param array $options Request options to apply to the given     *                       request and to the transfer. See \GuzzleHttp\RequestOptions.     */    public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface    {        // Merge the base URI into the request URI if needed.        $options = $this->prepareDefaults($options);        return $this->transfer(            $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')),            $options        );    }    /**     * Send an HTTP request.     *     * @param array $options Request options to apply to the given     *                       request and to the transfer. See \GuzzleHttp\RequestOptions.     *     * @throws GuzzleException     */    public function send(RequestInterface $request, array $options = []): ResponseInterface    {        $options[RequestOptions::SYNCHRONOUS] = true;        return $this->sendAsync($request, $options)->wait();    }    /**     * The HttpClient PSR (PSR-18) specify this method.     *

Annotations are widely adopted in Java[1] and are slowly being adopted into other languages [2], however, in most cases they break the fundamental Object-Oriented principal of encapsulation and limit flexibility in the process.

When used for configuration as opposed to documentation annotations cause a large set of problems. For example, a common use-case for annotations is @Inject to tell an external Dependency Injection Container to set a specific private property to the given dependency[3]:

 public class Product {
  @
Inject
  private Database db;
}

Here, @Inject signals to the Dependency Injection Container that a Database instance must be written to the private property. In a lot of common usage, no constructor is even supplied [4]. This means it's impossible to use the class without a dependency injection container.

The author of the class has written the code in the expectation that it will be created by a Dependency Injection Container. This breaks encapsulation as there is implied knowledge of other parts of the application within this class, knowledge that it will be used in an environment where a Dependency Injection Container looks for the annotation and supplies the dependency. This severely limits flexibility because there is no easy way to construct the class without using a Dependency Injection Container that understands @Inject and knows to look for it.

Encapsulation is broken because the class is no longer in control of its own state, it assumes that it will be running in a very specific environment.

If the class is moved to a project using a different container, or even no dependency injection container, it's usefulness is severely limited because there is no way to set the database dependency.

Instead, the code above should be written as:

 public class Product {
  private 
Database db;

  
Product(Database db) {
      
this.db db;
  }
}

Here, flexibility has been greatly enhanced because the class has no knowledge of the environment is is being used in. There may be a dependency injection container, there may not.

Encapsulation is now maintained, there is no way to instantiate the class without the constructor being run and dependencies supplied.

By using annotations, a dependency is needlessly introduced on the component which reads the annotations and the author of the class has indirect control over how external components can use the class. The class now has two responsibilities: exposing its own interface and telling the outside rules how it should be used. In the example above it is telling the container which dependencies it needs. These are two different responsibilities and this breaks the Single responsibility principleBy using annotations, a dependency is needlessly introduced on the component which reads the annotations and the author of the class has indirect control over how external components can use the class. The class now has two responsibilities: exposing its own interface and telling the outside rules how it should be used. In the example above it is telling the container which dependencies it needs. These are two different responsibilities and this breaks the [Single responsibility principle](#srp) and Encapsulation.

Allthough Inject is a common annotation, the problems exist anywhere annotations are used for application configuration.

They store metadata about the class and how the class should be used. This becomes a problem when different projects need different configurations for the class, for example annotations are commonly used for URL routing [5][6]:

 @Path("/users/{username}")
public class 
UserResource {

    @
GET
    @Produces("text/xml")
    public 
String getUser(@PathParam("username"String userName) {
        ...
    }
}

This is used to tell a web-server the URI the class will handle. In the example above, the annotation sets the route to users. This tight coupling of the metadata to the class causes flexibility issues. It's not unreasonable to want to use a class that deals with users on more than one website. However, because the class uses internal metadata using annotations, it's impossible to use the class on the URI /users on one website and /members/ on another without changing the class. This is a violation of the Single Responsibility Principle as the class has more than one reason to change.

If the class is changed then significant issues are introduced with version control: Once a bug is fixed on one website, it's then difficult to copy it over to the other website as the sites are using independent branches.

The solution is to separate out the metadata from the class using any format, for example JSON to map the URI path to the class name

 {
    
"/users""UserResource",
    
"/Products""Products"
}

Using external metadata, the metadata can differ per website and the class can remain identical, allowing for easier bugfixes and sharing of resources between projects.

As Ahuja[7] writes:

My advice is not to use annotations to configure your applications. Configurations were never meant to be part of code—that's why they're configurations and not source code. So let configurations stay in the configuration files. A small productivity gain in the short term won't go amount to much when a client asks for a change in a table or a value and you tell them it's going to take five days of development, testing and deployment.

There is an ongoing debate among developers about if and when annotations should be used. However, the sole benefit of annotations is being able to edit metadata and code in the same file, the argument in favour of their use is always at the expense of flexibility. This is an debate of convenience vs flexibility. From a flexibility point of view, annotations are considerably worse than available alternatives.

It should also be noted that this only applies to annotations which adjust the program's outcome. If removing the annotations does not affect how the program works, the annotations are there for documentation and do not cause any problems with flexibility.

As noted by Walls[8], annotations also break the law of demeter by adding extra dependencies to classes:

But, as luck would have it, SearchController also transitively depends on @Searchable and any other Compass annotations I use in Document. That means that I can't compile the web module without it having Compass in its build classpath (that is, as a dependency in its POM. Even though SearchController doesn't know about or need to know about Compass!
Doesn't this violate the law of demeter?

Although this problem is specific to Java, in other languages annotations will not introduce a hard dependency.

However, there is a remaining issue of comprehension. You could move a class with @Inject or similar annotation to a project where the annotations are just comments. Anyone looking at the class in this project will assume that the annotations are used and will be surprised when they change the annotations and it doesn't affect the configuration. This is not directly an issue of flexibility but it breaks the Principle of Least Surprise[9][10] and makes the class more difficult to reuse because it's not clear to anyone reading the code.

Summary of problems:

  • Cannot be debugged easily as you can't print the contents of the configuration
  • Finding application configuration requires looking across every class in the application
  • Breaks the Single Responsibility Principle
  • Breaks Encapsulation
  • Breaks Separation of Concerns
  • Introduces ambiguity/bugs in polymorphic code
  • Introduces coupling between unrelated components
  • Makes it more difficult to instantiate an object with different configurations
  • Makes version control more difficult

References

  1. Reigler, G. (2014) An Annotation Nightmare [online]. Available from: http://www.javacodegeeks.com/2014/01/an-annotation-nightmare.html
  2. Dohms, R. (2013) Annotations in PHP: They Exist [online]. Available from: http://www.slideshare.net/rdohms/annotations-in-php-they-exist
  3. Oracle, O. (2011) Annotation Type Inject [online]. Available from: http://docs.oracle.com/javaee/6/api/javax/inject/Inject.html
  4. Bugayenko, Y. (2016) Java Annotations Are a Big Mistake [online]. Available from: http://www.yegor256.com/2016/04/12/java-annotations-are-evil.html
  5. Oracle, O. (2010) The @Path Annotation and URI Path Templates [online]. Available from: https://docs.oracle.com/cd/E19798-01/821-1841/ginpw/
  6. Symfony Framework, S. (nd) @Route and @Method [online]. Available from: http://symfony.com/doc/2.0/bundles/SensioFrameworkExtraBundle/annotations/routing.html
  7. Ahuja, K. (2015) Are Annotations Bad? [online]. Available from: https://dzone.com/articles/are-annotations-bad
  8. Walls, C. (2008) When Good Annotations Go Bad [online]. Available from: http://java.dzone.com/articles/when-good-annotations-go-bad
  9. Raymond, E. (2003) The Art of Unix Programming ISBN: 978-0131429017. Addison Wesley.
  10. James, G. (1987) The Tao of Programming ISBN: 978-0931137075. Info Books.
*/ public function sendRequest(RequestInterface $request): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; $options[RequestOptions::ALLOW_REDIRECTS] = false; $options[RequestOptions::HTTP_ERRORS] = false; return $this->sendAsync($request, $options)->wait(); } /** * Create and send an asynchronous HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. Use an array to provide a URL * template and additional variables to use in the URL template expansion. * * @param string $method HTTP method * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. */ public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface { $options = $this->prepareDefaults($options); // Remove request modifying parameter because it can be done up-front. $headers = isset($options['headers']) ? $options['headers'] : []; $body = isset($options['body']) ? $options['body'] : null; $version = isset($options['version']) ? $options['version'] : '1.1'; // Merge the URI into the base URI. $uri = $this->buildUri(Psr7\uri_for($uri), $options); if (\is_array($body)) { throw $this->invalidBody(); } $request = new Psr7\Request($method, $uri, $headers, $body, $version); // Remove the option so that they are not doubly-applied. unset($options['headers'], $options['body'], $options['version']); return $this->transfer($request, $options); } /** * Create and send an HTTP request. * * Use an absolute path to override the base path of the client, or a * relative path to append to the base path of the client. The URL can * contain the query string as well. * * @param string $method HTTP method. * @param string|UriInterface $uri URI object or string. * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. * * @throws GuzzleException */ public function request(string $method, $uri = '', array $options = []): ResponseInterface { $options[RequestOptions::SYNCHRONOUS] = true; return $this->requestAsync($method, $uri, $options)->wait(); } /** * Get a client configuration option. * * These options include default request options of the client, a "handler" * (if utilized by the concrete client), and a "base_uri" if utilized by * the concrete client. * * @param string|null $option The config option to retrieve. * * @return mixed */ public function getConfig(?string $option = null) { return $option === null ? $this->config : (isset($this->config[$option]) ? $this->config[$option] : null); } private function buildUri(UriInterface $uri, array $config): UriInterface { if (isset($config['base_uri'])) { $uri = Psr7\UriResolver::resolve(Psr7\uri_for($config['base_uri']), $uri); } if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { $idnOptions = ($config['idn_conversion'] === true) ? IDNA_DEFAULT : $config['idn_conversion']; $uri = Utils::idnUriConvert($uri, $idnOptions); } return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; } /** * Configures the default options for a client. */ private function configureDefaults(array $config): void { $defaults = [ 'allow_redirects' => RedirectMiddleware::$defaultSettings, 'http_errors' => true, 'decode_content' => true, 'verify' => true, 'cookies' => false ]; // idn_to_ascii() is a part of ext-intl and might be not available // Old ICU versions don't have this constant, so we are basically stuck (see https://github.com/guzzle/guzzle/pull/2424 // and https://github.com/guzzle/guzzle/issues/2448 for details) $defaults['idn_conversion'] = \function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46'); // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. // We can only trust the HTTP_PROXY environment variable in a CLI // process due to the fact that PHP has no reliable mechanism to // get environment variables that start with "HTTP_". if (PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { $defaults['proxy']['http'] = $proxy; } if ($proxy = Utils::getenv('HTTPS_PROXY')) { $defaults['proxy']['https'] = $proxy; } if ($noProxy = Utils::getenv('NO_PROXY')) { $cleanedNoProxy = \str_replace(' ', '', $noProxy); $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); } $this->config = $config + $defaults; if (!empty($config['cookies']) && $config['cookies'] === true) { $this->config['cookies'] = new CookieJar(); } // Add the default user-agent header. if (!isset($this->config['headers'])) { $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; } else { // Add the User-Agent header if one was not already set. foreach (\array_keys($this->config['headers']) as $name) { if (\strtolower($name) === 'user-agent') { return; } } $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); } } /** * Merges default options into the array. * * @param array $options Options to modify by reference */ private function prepareDefaults(array $options): array { $defaults = $this->config; if (!empty($defaults['headers'])) { // Default headers are only added if they are not present. $defaults['_conditional'] = $defaults['headers']; unset($defaults['headers']); } // Special handling for headers is required as they are added as // conditional headers and as headers passed to a request ctor. if (\array_key_exists('headers', $options)) { // Allows default headers to be unset. if ($options['headers'] === null) { $defaults['_conditional'] = []; unset($options['headers']); } elseif (!\is_array($options['headers'])) { throw new InvalidArgumentException('headers must be an array'); } } // Shallow merge defaults underneath options. $result = $options + $defaults; // Remove null values. foreach ($result as $k => $v) { if ($v === null) { unset($result[$k]); } } return $result; } /** * Transfers the given request and applies request options. * * The URI of the request is not modified and the request options are used * as-is without merging in default options. * * @param array $options See \GuzzleHttp\RequestOptions. */ private function transfer(RequestInterface $request, array $options): PromiseInterface { $request = $this->applyOptions($request, $options); /** @var HandlerStack $handler */ $handler = $options['handler']; try { return Promise\promise_for($handler($request, $options)); } catch (\Exception $e) { return Promise\rejection_for($e); } } /** * Applies the array of request options to a request. */ private function applyOptions(RequestInterface $request, array &$options): RequestInterface { $modify = [ 'set_headers' => [], ]; if (isset($options['headers'])) { $modify['set_headers'] = $options['headers']; unset($options['headers']); } if (isset($options['form_params'])) { if (isset($options['multipart'])) { throw new InvalidArgumentException('You cannot use ' . 'form_params and multipart at the same time. Use the ' . 'form_params option if you want to send application/' . 'x-www-form-urlencoded requests, and the multipart ' . 'option to send multipart/form-data requests.'); } $options['body'] = \http_build_query($options['form_params'], '', '&'); unset($options['form_params']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; } if (isset($options['multipart'])) { $options['body'] = new Psr7\MultipartStream($options['multipart']); unset($options['multipart']); } if (isset($options['json'])) { $options['body'] = Utils::jsonEncode($options['json']); unset($options['json']); // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'application/json'; } if (!empty($options['decode_content']) && $options['decode_content'] !== true ) { // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\_caseless_remove(['Accept-Encoding'], $options['_conditional']); $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; } if (isset($options['body'])) { if (\is_array($options['body'])) { throw $this->invalidBody(); } $modify['body'] = Psr7\stream_for($options['body']); unset($options['body']); } if (!empty($options['auth']) && \is_array($options['auth'])) { $value = $options['auth']; $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; switch ($type) { case 'basic': // Ensure that we don't have the header in different case and set the new value. $modify['set_headers'] = Psr7\_caseless_remove(['Authorization'], $modify['set_headers']); $modify['set_headers']['Authorization'] = 'Basic ' . \base64_encode("$value[0]:$value[1]"); break; case 'digest': // @todo: Do not rely on curl $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; case 'ntlm': $options['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_NTLM; $options['curl'][CURLOPT_USERPWD] = "$value[0]:$value[1]"; break; } } if (isset($options['query'])) { $value = $options['query']; if (\is_array($value)) { $value = \http_build_query($value, null, '&', PHP_QUERY_RFC3986); } if (!\is_string($value)) { throw new InvalidArgumentException('query must be a string or array'); } $modify['query'] = $value; unset($options['query']); } // Ensure that sink is not an invalid value. if (isset($options['sink'])) { // TODO: Add more sink validation? if (\is_bool($options['sink'])) { throw new InvalidArgumentException('sink must not be a boolean'); } } $request = Psr7\modify_request($request, $modify); if ($request->getBody() instanceof Psr7\MultipartStream) { // Use a multipart/form-data POST if a Content-Type is not set. // Ensure that we don't have the header in different case and set the new value. $options['_conditional'] = Psr7\_caseless_remove(['Content-Type'], $options['_conditional']); $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' . $request->getBody()->getBoundary(); } // Merge in conditional headers if they are not present. if (isset($options['_conditional'])) { // Build up the changes so it's in a single clone of the message. $modify = []; foreach ($options['_conditional'] as $k => $v) { if (!$request->hasHeader($k)) { $modify['set_headers'][$k] = $v; } } $request = Psr7\modify_request($request, $modify); // Don't pass this internal value along to middleware/handlers. unset($options['_conditional']); } return $request; } /** * Return an InvalidArgumentException with pre-set message. */ private function invalidBody(): InvalidArgumentException { return new InvalidArgumentException('Passing in the "body" request ' . 'option as an array to send a request is not supported. ' . 'Please use the "form_params" request option to send a ' . 'application/x-www-form-urlencoded request, or the "multipart" ' . 'request option to send a multipart/form-data request.'); }}