startApp(); } return Yii::$app; } public function resetApplication() { codecept_debug('Destroying application'); Yii::$app = null; \yii\web\UploadedFile::reset(); if (method_exists(\yii\base\Event::className(), 'offAll')) { \yii\base\Event::offAll(); } Yii::setLogger(null); // This resolves an issue with database connections not closing properly. gc_collect_cycles(); } public function startApp() { codecept_debug('Starting application'); $config = require($this->configFile); if (!isset($config['class'])) { $config['class'] = 'yii\web\Application'; } $config = $this->mockMailer($config); /** @var \yii\web\Application $app */ Yii::$app = Yii::createObject($config); Yii::setLogger(new Logger()); } /** * * @param \Symfony\Component\BrowserKit\Request $request * * @return \Symfony\Component\BrowserKit\Response */ public function doRequest($request) { $_COOKIE = $request->getCookies(); $_SERVER = $request->getServer(); $_FILES = $this->remapFiles($request->getFiles()); $_REQUEST = $this->remapRequestParameters($request->getParameters()); $_POST = $_GET = []; if (strtoupper($request->getMethod()) === 'GET') { $_GET = $_REQUEST; } else { $_POST = $_REQUEST; } $uri = $request->getUri(); $pathString = parse_url($uri, PHP_URL_PATH); $queryString = parse_url($uri, PHP_URL_QUERY); $_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString; $_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod()); parse_str($queryString, $params); foreach ($params as $k => $v) { $_GET[$k] = $v; } ob_start(); $this->beforeRequest(); $app = $this->getApplication(); // disabling logging. Logs are slowing test execution down foreach ($app->log->targets as $target) { $target->enabled = false; } $yiiRequest = $app->getRequest(); if ($request->getContent() !== null) { $yiiRequest->setRawBody($request->getContent()); $yiiRequest->setBodyParams(null); } else { $yiiRequest->setRawBody(null); $yiiRequest->setBodyParams($_POST); } $yiiRequest->setQueryParams($_GET); try { /* * This is basically equivalent to $app->run() without sending the response. * Sending the response is problematic because it tries to send headers. */ $app->trigger($app::EVENT_BEFORE_REQUEST); $response = $app->handleRequest($yiiRequest); $app->trigger($app::EVENT_AFTER_REQUEST); $response->send(); } catch (\Exception $e) { if ($e instanceof HttpException) { // Don't discard output and pass exception handling to Yii to be able // to expect error response codes in tests. $app->errorHandler->discardExistingOutput = false; $app->errorHandler->handleException($e); } elseif (!$e instanceof ExitException) { // for exceptions not related to Http, we pass them to Codeception throw $e; } $response = $app->response; } $this->encodeCookies($response, $yiiRequest, $app->security); if ($response->isRedirection) { Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true)); } $content = ob_get_clean(); if (empty($content) && !empty($response->content)) { throw new \Exception('No content was sent from Yii application'); } return new Response($content, $response->statusCode, $response->getHeaders()->toArray()); } protected function revertErrorHandler() { $handler = new ErrorHandler(); set_error_handler([$handler, 'errorHandler']); } /** * Encodes the cookies and adds them to the headers. * @param \yii\web\Response $response * @throws \yii\base\InvalidConfigException */ protected function encodeCookies( YiiResponse $response, Request $request, Security $security ) { if ($request->enableCookieValidation) { $validationKey = $request->cookieValidationKey; } foreach ($response->getCookies() as $cookie) { /** @var \yii\web\Cookie $cookie */ $value = $cookie->value; if ($cookie->expire != 1 && isset($validationKey)) { $data = version_compare(Yii::getVersion(), '2.0.2', '>') ? [$cookie->name, $cookie->value] : $cookie->value; $value = $security->hashData(serialize($data), $validationKey); } $c = new Cookie( $cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly ); $this->getCookieJar()->set($c); } } /** * Replace mailer with in memory mailer * @param array $config Original configuration * @return array New configuration */ protected function mockMailer(array $config) { // options that make sense for mailer mock $allowedOptions = [ 'htmlLayout', 'textLayout', 'messageConfig', 'messageClass', 'useFileTransport', 'fileTransportPath', 'fileTransportCallback', 'view', 'viewPath', ]; $mailerConfig = [ 'class' => 'Codeception\Lib\Connector\Yii2\TestMailer', ]; if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) { foreach ($config['components']['mailer'] as $name => $value) { if (in_array($name, $allowedOptions, true)) { $mailerConfig[$name] = $value; } } } $config['components']['mailer'] = $mailerConfig; return $config; } public function restart() { parent::restart(); $this->resetApplication(); } /** * Resets the applications' response object. * The method used depends on the module configuration. */ protected function resetResponse(Application $app) { $method = $this->responseCleanMethod; // First check the current response object. if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE) || count($app->response->getBehaviors()) > 0 ) && $method === self::CLEAN_RECREATE ) { Debug::debug(<<set('response', $app->getComponents()['response']); break; case self::CLEAN_CLEAR: $app->response->clear(); break; case self::CLEAN_MANUAL: break; } } protected function resetRequest(Application $app) { $method = $this->requestCleanMethod; $request = $app->request; // First check the current request object. if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) { Debug::debug(<<set('request', $app->getComponents()['request']); break; case self::CLEAN_CLEAR: $request->getHeaders()->removeAll(); $request->setBaseUrl(null); $request->setHostInfo(null); $request->setPathInfo(null); $request->setScriptFile(null); $request->setScriptUrl(null); $request->setUrl(null); $request->setPort(null); $request->setSecurePort(null); $request->setAcceptableContentTypes(null); $request->setAcceptableLanguages(null); break; case self::CLEAN_MANUAL: break; } } /** * Called before each request, preparation happens here. */ protected function beforeRequest() { if ($this->recreateApplication) { $this->resetApplication(); return; } $application = $this->getApplication(); $this->resetResponse($application); $this->resetRequest($application); $definitions = $application->getComponents(true); foreach ($this->recreateComponents as $component) { // Only recreate if it has actually been instantiated. if ($application->has($component, true)) { $application->set($component, $definitions[$component]); } } } }