Yii2::doRequest()   F
last analyzed

Complexity

Conditions 12
Paths 1280

Size

Total Lines 85

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 1280
nop 1
dl 0
loc 85
rs 2.5672
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Codeception\Lib\Connector;
3
4
use Codeception\Lib\Connector\Yii2\Logger;
5
use Codeception\Lib\InnerBrowser;
6
use Codeception\Util\Debug;
7
use Symfony\Component\BrowserKit\Client;
8
use Symfony\Component\BrowserKit\Cookie;
9
use Symfony\Component\BrowserKit\Response;
10
use Yii;
11
use yii\base\ExitException;
12
use yii\base\Security;
13
use yii\web\Application;
14
use yii\web\ErrorHandler;
15
use yii\web\HttpException;
16
use yii\web\Request;
17
use yii\web\Response as YiiResponse;
18
19
class Yii2 extends Client
20
{
21
    use Shared\PhpSuperGlobalsConverter;
22
23
    const CLEAN_METHODS = [
24
        self::CLEAN_RECREATE,
25
        self::CLEAN_CLEAR,
26
        self::CLEAN_FORCE_RECREATE,
27
        self::CLEAN_MANUAL
28
    ];
29
    /**
30
     * Clean the response object by recreating it.
31
     * This might lose behaviors / event handlers / other changes that are done in the application bootstrap phase.
32
     */
33
    const CLEAN_RECREATE = 'recreate';
34
    /**
35
     * Same as recreate but will not warn when behaviors / event handlers are lost.
36
     */
37
    const CLEAN_FORCE_RECREATE = 'force_recreate';
38
    /**
39
     * Clean the response object by resetting specific properties via its' `clear()` method.
40
     * This will keep behaviors / event handlers, but could inadvertently leave some changes intact.
41
     * @see \Yii\web\Response::clear()
42
     */
43
    const CLEAN_CLEAR = 'clear';
44
45
    /**
46
     * Do not clean the response, instead the test writer will be responsible for manually resetting the response in
47
     * between requests during one test
48
     */
49
    const CLEAN_MANUAL = 'manual';
50
51
52
    /**
53
     * @var string application config file
54
     */
55
    public $configFile;
56
57
    /**
58
     * @var string method for cleaning the response object before each request
59
     */
60
    public $responseCleanMethod;
61
62
    /**
63
     * @var string method for cleaning the request object before each request
64
     */
65
    public $requestCleanMethod;
66
67
    /**
68
     * @var string[] List of component names that must be recreated before each request
69
     */
70
    public $recreateComponents = [];
71
72
    /**
73
     * This option is there primarily for backwards compatibility.
74
     * It means you cannot make any modification to application state inside your app, since they will get discarded.
75
     * @var bool whether to recreate the whole application before each request
76
     */
77
    public $recreateApplication = false;
78
79
    /**
80
     * @return \yii\web\Application
81
     */
82
    public function getApplication()
83
    {
84
        if (!isset(Yii::$app)) {
85
            $this->startApp();
86
        }
87
        return Yii::$app;
88
    }
89
90
    public function resetApplication()
91
    {
92
        codecept_debug('Destroying application');
93
        Yii::$app = null;
94
        \yii\web\UploadedFile::reset();
95
        if (method_exists(\yii\base\Event::className(), 'offAll')) {
96
            \yii\base\Event::offAll();
97
        }
98
        Yii::setLogger(null);
99
        // This resolves an issue with database connections not closing properly.
100
        gc_collect_cycles();
101
    }
102
103
    public function startApp()
104
    {
105
        codecept_debug('Starting application');
106
        $config = require($this->configFile);
107
        if (!isset($config['class'])) {
108
            $config['class'] = 'yii\web\Application';
109
        }
110
111
        $config = $this->mockMailer($config);
112
        /** @var \yii\web\Application $app */
113
        Yii::$app = Yii::createObject($config);
114
115
        Yii::setLogger(new Logger());
116
    }
117
118
    /**
119
     *
120
     * @param \Symfony\Component\BrowserKit\Request $request
121
     *
122
     * @return \Symfony\Component\BrowserKit\Response
123
     */
124
    public function doRequest($request)
125
    {
126
        $_COOKIE = $request->getCookies();
127
        $_SERVER = $request->getServer();
128
        $_FILES = $this->remapFiles($request->getFiles());
129
        $_REQUEST = $this->remapRequestParameters($request->getParameters());
130
        $_POST = $_GET = [];
131
132
        if (strtoupper($request->getMethod()) === 'GET') {
133
            $_GET = $_REQUEST;
134
        } else {
135
            $_POST = $_REQUEST;
136
        }
137
138
        $uri = $request->getUri();
139
140
        $pathString = parse_url($uri, PHP_URL_PATH);
141
        $queryString = parse_url($uri, PHP_URL_QUERY);
142
        $_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString;
143
        $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
144
145
        parse_str($queryString, $params);
146
        foreach ($params as $k => $v) {
0 ignored issues
show
Bug introduced by
The expression $params of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
147
            $_GET[$k] = $v;
148
        }
149
150
        ob_start();
151
152
        $this->beforeRequest();
153
154
        $app = $this->getApplication();
155
156
        // disabling logging. Logs are slowing test execution down
157
        foreach ($app->log->targets as $target) {
158
            $target->enabled = false;
159
        }
160
161
162
163
164
        $yiiRequest = $app->getRequest();
165
        if ($request->getContent() !== null) {
166
            $yiiRequest->setRawBody($request->getContent());
167
            $yiiRequest->setBodyParams(null);
168
        } else {
169
            $yiiRequest->setRawBody(null);
170
            $yiiRequest->setBodyParams($_POST);
171
        }
172
        $yiiRequest->setQueryParams($_GET);
173
174
        try {
175
            /*
176
             * This is basically equivalent to $app->run() without sending the response.
177
             * Sending the response is problematic because it tries to send headers.
178
             */
179
            $app->trigger($app::EVENT_BEFORE_REQUEST);
180
            $response = $app->handleRequest($yiiRequest);
181
            $app->trigger($app::EVENT_AFTER_REQUEST);
182
            $response->send();
183
        } catch (\Exception $e) {
184
            if ($e instanceof HttpException) {
185
                // Don't discard output and pass exception handling to Yii to be able
186
                // to expect error response codes in tests.
187
                $app->errorHandler->discardExistingOutput = false;
188
                $app->errorHandler->handleException($e);
189
            } elseif (!$e instanceof ExitException) {
190
                // for exceptions not related to Http, we pass them to Codeception
191
                throw $e;
192
            }
193
            $response = $app->response;
194
        }
195
196
        $this->encodeCookies($response, $yiiRequest, $app->security);
197
198
        if ($response->isRedirection) {
199
            Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true));
200
        }
201
202
        $content = ob_get_clean();
203
        if (empty($content) && !empty($response->content)) {
204
            throw new \Exception('No content was sent from Yii application');
205
        }
206
207
        return new Response($content, $response->statusCode, $response->getHeaders()->toArray());
208
    }
209
210
    protected function revertErrorHandler()
211
    {
212
        $handler = new ErrorHandler();
213
        set_error_handler([$handler, 'errorHandler']);
214
    }
215
216
217
    /**
218
     * Encodes the cookies and adds them to the headers.
219
     * @param \yii\web\Response $response
220
     * @throws \yii\base\InvalidConfigException
221
     */
222
    protected function encodeCookies(
223
        YiiResponse $response,
224
        Request $request,
225
        Security $security
226
    ) {
227
        if ($request->enableCookieValidation) {
228
            $validationKey = $request->cookieValidationKey;
229
        }
230
231
        foreach ($response->getCookies() as $cookie) {
232
            /** @var \yii\web\Cookie $cookie */
233
            $value = $cookie->value;
234
            if ($cookie->expire != 1 && isset($validationKey)) {
235
                $data = version_compare(Yii::getVersion(), '2.0.2', '>')
236
                    ? [$cookie->name, $cookie->value]
237
                    : $cookie->value;
238
                $value = $security->hashData(serialize($data), $validationKey);
239
            }
240
            $c = new Cookie(
241
                $cookie->name,
242
                $value,
243
                $cookie->expire,
244
                $cookie->path,
245
                $cookie->domain,
246
                $cookie->secure,
247
                $cookie->httpOnly
248
            );
249
            $this->getCookieJar()->set($c);
250
        }
251
    }
252
253
    /**
254
     * Replace mailer with in memory mailer
255
     * @param array $config Original configuration
256
     * @return array New configuration
257
     */
258
    protected function mockMailer(array $config)
259
    {
260
        // options that make sense for mailer mock
261
        $allowedOptions = [
262
            'htmlLayout',
263
            'textLayout',
264
            'messageConfig',
265
            'messageClass',
266
            'useFileTransport',
267
            'fileTransportPath',
268
            'fileTransportCallback',
269
            'view',
270
            'viewPath',
271
        ];
272
273
        $mailerConfig = [
274
            'class' => 'Codeception\Lib\Connector\Yii2\TestMailer',
275
        ];
276
277
        if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) {
278
            foreach ($config['components']['mailer'] as $name => $value) {
279
                if (in_array($name, $allowedOptions, true)) {
280
                    $mailerConfig[$name] = $value;
281
                }
282
            }
283
        }
284
        $config['components']['mailer'] = $mailerConfig;
285
286
        return $config;
287
    }
288
289
    public function restart()
290
    {
291
        parent::restart();
292
        $this->resetApplication();
293
    }
294
295
    /**
296
     * Resets the applications' response object.
297
     * The method used depends on the module configuration.
298
     */
299
    protected function resetResponse(Application $app)
300
    {
301
        $method = $this->responseCleanMethod;
302
        // First check the current response object.
303
        if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND)
304
                || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND)
305
                || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE)
306
                || count($app->response->getBehaviors()) > 0
307
            ) && $method === self::CLEAN_RECREATE
308
        ) {
309
            Debug::debug(<<<TEXT
310
[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
311
the response object, this means any behaviors or events that are not attached in the component config will be lost.
312
We will fall back to clearing the response. If you are certain you want to recreate it, please configure 
313
responseCleanMethod = 'force_recreate' in the module.  
314
TEXT
315
            );
316
            $method = self::CLEAN_CLEAR;
317
        }
318
319
        switch ($method) {
320
            case self::CLEAN_FORCE_RECREATE:
321
            case self::CLEAN_RECREATE:
322
                $app->set('response', $app->getComponents()['response']);
323
                break;
324
            case self::CLEAN_CLEAR:
325
                $app->response->clear();
326
                break;
327
            case self::CLEAN_MANUAL:
328
                break;
329
        }
330
    }
331
332
    protected function resetRequest(Application $app)
333
    {
334
        $method = $this->requestCleanMethod;
335
        $request = $app->request;
336
337
        // First check the current request object.
338
        if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) {
339
            Debug::debug(<<<TEXT
340
[WARNING] You are attaching event handlers or behaviors to the request object. But the Yii2 module is configured to recreate
341
the request object, this means any behaviors or events that are not attached in the component config will be lost.
342
We will fall back to clearing the request. If you are certain you want to recreate it, please configure 
343
requestCleanMethod = 'force_recreate' in the module.  
344
TEXT
345
            );
346
            $method = self::CLEAN_CLEAR;
347
        }
348
349
        switch ($method) {
350
            case self::CLEAN_FORCE_RECREATE:
351
            case self::CLEAN_RECREATE:
352
                $app->set('request', $app->getComponents()['request']);
353
                break;
354
            case self::CLEAN_CLEAR:
355
                $request->getHeaders()->removeAll();
356
                $request->setBaseUrl(null);
357
                $request->setHostInfo(null);
358
                $request->setPathInfo(null);
359
                $request->setScriptFile(null);
360
                $request->setScriptUrl(null);
361
                $request->setUrl(null);
362
                $request->setPort(null);
363
                $request->setSecurePort(null);
364
                $request->setAcceptableContentTypes(null);
365
                $request->setAcceptableLanguages(null);
366
367
                break;
368
            case self::CLEAN_MANUAL:
369
                break;
370
        }
371
    }
372
373
    /**
374
     * Called before each request, preparation happens here.
375
     */
376
    protected function beforeRequest()
377
    {
378
        if ($this->recreateApplication) {
379
            $this->resetApplication();
380
            return;
381
        }
382
383
        $application = $this->getApplication();
384
385
        $this->resetResponse($application);
386
        $this->resetRequest($application);
387
388
        $definitions = $application->getComponents(true);
389
        foreach ($this->recreateComponents as $component) {
390
            // Only recreate if it has actually been instantiated.
391
            if ($application->has($component, true)) {
392
                $application->set($component, $definitions[$component]);
393
            }
394
        }
395
    }
396
}
397