Completed
Push — master ( f71893...9947c7 )
by David
01:29
created

Client::__construct()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 5.9256

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 2
cts 3
cp 0.6667
rs 9.6111
c 0
b 0
f 0
cc 5
nc 3
nop 1
crap 5.9256
1
<?php
2
3
namespace Http\Mock;
4
5
use Http\Client\Common\HttpAsyncClientEmulator;
6
use Http\Client\Common\VersionBridgeClient;
7
use Http\Client\Exception;
8
use Http\Client\HttpAsyncClient;
9
use Http\Client\HttpClient;
10
use Http\Discovery\MessageFactoryDiscovery;
11
use Http\Message\RequestMatcher;
12
use Http\Message\ResponseFactory;
13
use Psr\Http\Client\ClientExceptionInterface;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseFactoryInterface;
16
use Psr\Http\Message\ResponseInterface;
17
18
/**
19
 * An implementation of the HTTP client that is useful for automated tests.
20
 *
21
 * This mock does not send requests but stores them for later retrieval.
22
 * You can configure the mock with responses to return and/or exceptions to throw.
23
 *
24
 * @author David de Boer <[email protected]>
25
 */
26
class Client implements HttpClient, HttpAsyncClient
27
{
28
    use HttpAsyncClientEmulator;
29
    use VersionBridgeClient;
30
31
    /**
32
     * @var ResponseFactory|ResponseFactoryInterface
33
     */
34
    private $responseFactory;
35
36
    /**
37
     * @var array
38
     */
39
    private $conditionalResults = [];
40
41
    /**
42
     * @var RequestInterface[]
43
     */
44
    private $requests = [];
45
46
    /**
47
     * @var ResponseInterface[]
48
     */
49
    private $responses = [];
50
51
    /**
52
     * @var ResponseInterface|null
53
     */
54
    private $defaultResponse;
55
56
    /**
57
     * @var Exception[]
58
     */
59
    private $exceptions = [];
60
61
    /**
62
     * @var Exception|null
63
     */
64
    private $defaultException;
65
66
    /**
67
     * @param ResponseFactory|ResponseFactoryInterface|null
68
     */
69 15
    public function __construct($responseFactory = null)
70
    {
71 15
        if (!$responseFactory instanceof ResponseFactory && !$responseFactory instanceof ResponseFactoryInterface && null !== $responseFactory) {
72
            throw new \TypeError(
73
                sprintf('%s::__construct(): Argument #1 ($responseFactory) must be of type %s|%s|null, %s given', self::class, ResponseFactory::class, ResponseFactoryInterface::class, get_debug_type($responseFactory))
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('%s::__construct...type($responseFactory)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
74
            );
75
        }
76
77
        $this->responseFactory = $responseFactory ?: MessageFactoryDiscovery::find();
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function doSendRequest(RequestInterface $request)
84
    {
85
        $this->requests[] = $request;
86
87
        foreach ($this->conditionalResults as $result) {
88
            /**
89
             * @var RequestMatcher
90
             */
91
            $matcher = $result['matcher'];
92
93
            /**
94
             * @var callable
95
             */
96
            $callable = $result['callable'];
97
98
            if ($matcher->matches($request)) {
99
                return $callable($request);
100
            }
101
        }
102
103
        if (count($this->exceptions) > 0) {
104
            throw array_shift($this->exceptions);
105
        }
106
107
        if (count($this->responses) > 0) {
108
            return array_shift($this->responses);
109
        }
110
111
        if ($this->defaultException) {
112
            throw $this->defaultException;
113
        }
114
115
        if ($this->defaultResponse) {
116
            return $this->defaultResponse;
117
        }
118
119
        // Return success response by default
120
        return $this->responseFactory->createResponse();
121
    }
122
123
    /**
124
     * Adds an exception to be thrown or response to be returned if the request
125
     * matcher matches.
126
     *
127
     * For more complex logic, pass a callable as $result. The method is given
128
     * the request and MUST either return a ResponseInterface or throw an
129
     * exception that implements the PSR-18 / HTTPlug exception interface.
130
     *
131
     * @param ResponseInterface|Exception|ClientExceptionInterface|callable $result
132
     */
133
    public function on(RequestMatcher $requestMatcher, $result)
134
    {
135
        if (!$result instanceof ResponseInterface && !$result instanceof Exception && !$result instanceof ClientExceptionInterface && !is_callable($result)) {
136
            throw new \TypeError(
137
                sprintf('%s::on(): Argument #2 ($result) must be of type %s|%s|%s|callable, %s given', self::class, ResponseInterface::class, Exception::class, ClientExceptionInterface::class, get_debug_type($result))
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('%s::on(): Argum...et_debug_type($result)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
138
            );
139
        }
140
141
        $callable = self::makeCallable($result);
142
143
        $this->conditionalResults[] = [
144
            'matcher' => $requestMatcher,
145
            'callable' => $callable,
146
        ];
147
    }
148
149
    /**
150
     * @param ResponseInterface|Exception|ClientExceptionInterface|callable $result
151
     *
152
     * @return callable
153
     */
154
    private static function makeCallable($result)
155
    {
156
        if (is_callable($result)) {
157
            return $result;
158
        }
159
160
        if ($result instanceof ResponseInterface) {
161
            return function () use ($result) {
162
                return $result;
163
            };
164
        }
165
166
        return function () use ($result) {
167
            throw $result;
168
        };
169
    }
170
171
    /**
172
     * Adds an exception that will be thrown.
173
     */
174
    public function addException(\Exception $exception)
175
    {
176 View Code Duplication
        if (!$exception instanceof Exception) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
            @trigger_error('Clients may only throw exceptions of type '.Exception::class.'. Setting an exception of class '.get_class($exception).' will not be possible anymore in the future', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
178
        }
179
        $this->exceptions[] = $exception;
180
    }
181
182
    /**
183
     * Sets the default exception to throw when the list of added exceptions and responses is exhausted.
184
     *
185
     * If both a default exception and a default response are set, the exception will be thrown.
186
     */
187
    public function setDefaultException(\Exception $defaultException = null)
188
    {
189 View Code Duplication
        if (null !== $defaultException && !$defaultException instanceof Exception) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190
            @trigger_error('Clients may only throw exceptions of type '.Exception::class.'. Setting an exception of class '.get_class($defaultException).' will not be possible anymore in the future', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
191
        }
192
        $this->defaultException = $defaultException;
0 ignored issues
show
Documentation Bug introduced by
It seems like $defaultException can also be of type object<Exception>. However, the property $defaultException is declared as type object<Http\Client\Exception>|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
193
    }
194
195
    /**
196
     * Adds a response that will be returned in first in first out order.
197
     */
198
    public function addResponse(ResponseInterface $response)
199
    {
200
        $this->responses[] = $response;
201
    }
202
203
    /**
204
     * Sets the default response to be returned when the list of added exceptions and responses is exhausted.
205
     */
206
    public function setDefaultResponse(ResponseInterface $defaultResponse = null)
207
    {
208
        $this->defaultResponse = $defaultResponse;
209
    }
210
211
    /**
212
     * Returns requests that were sent.
213
     *
214
     * @return RequestInterface[]
215
     */
216
    public function getRequests()
217
    {
218
        return $this->requests;
219
    }
220
221
    public function getLastRequest()
222
    {
223
        return end($this->requests);
224
    }
225
226
    public function reset()
227
    {
228
        $this->conditionalResults = [];
229
        $this->responses = [];
230
        $this->exceptions = [];
231
        $this->requests = [];
232
        $this->setDefaultException();
233
        $this->setDefaultResponse();
234
    }
235
}
236