55namespace Codeception \Lib \Connector ;
66
77use Codeception \Exception \ConfigurationException ;
8+ use Codeception \Exception \ModuleConfigException ;
89use Codeception \Lib \Connector \Yii2 \Logger ;
910use Codeception \Lib \Connector \Yii2 \TestMailer ;
1011use Codeception \Util \Debug ;
1314use Symfony \Component \BrowserKit \Cookie ;
1415use Symfony \Component \BrowserKit \CookieJar ;
1516use Symfony \Component \BrowserKit \History ;
17+ use Symfony \Component \BrowserKit \Request as BrowserkitRequest ;
18+ use yii \web \Request as YiiRequest ;
1619use Symfony \Component \BrowserKit \Response ;
1720use Yii ;
21+ use yii \base \Component ;
22+ use yii \base \Event ;
1823use yii \base \ExitException ;
1924use yii \base \Security ;
2025use yii \base \UserException ;
26+ use yii \mail \BaseMessage ;
2127use yii \mail \MessageInterface ;
2228use yii \web \Application ;
2329use yii \web \ErrorHandler ;
2632use yii \web \Response as YiiResponse ;
2733use yii \web \User ;
2834
35+
36+ /**
37+ * @extends Client<BrowserkitRequest, Response>
38+ */
2939class Yii2 extends Client
3040{
3141 use Shared \PhpSuperGlobalsConverter;
@@ -98,18 +108,29 @@ class Yii2 extends Client
98108 public string |null $ applicationClass = null ;
99109
100110
111+ /**
112+ * @var list<BaseMessage>
113+ */
101114 private array $ emails = [];
102115
103116 /**
104- * @deprecated since 2.5, will become protected in 3.0. Directly access to \Yii::$app if you need to interact with it.
105117 * @internal
106118 */
107- public function getApplication (): \yii \base \Application
119+ protected function getApplication (): \yii \base \Application
108120 {
109121 if (!isset (Yii::$ app )) {
110122 $ this ->startApp ();
111123 }
112- return Yii::$ app ;
124+ return Yii::$ app ?? throw new \RuntimeException ('Failed to create Yii2 application ' );
125+ }
126+
127+ private function getWebRequest (): YiiRequest
128+ {
129+ $ request = $ this ->getApplication ()->request ;
130+ if (!$ request instanceof YiiRequest) {
131+ throw new \RuntimeException ('Request component is not of type ' . YiiRequest::class);
132+ }
133+ return $ request ;
113134 }
114135
115136 public function resetApplication (bool $ closeSession = true ): void
@@ -120,9 +141,7 @@ public function resetApplication(bool $closeSession = true): void
120141 }
121142 Yii::$ app = null ;
122143 \yii \web \UploadedFile::reset ();
123- if (method_exists (\yii \base \Event::class, 'offAll ' )) {
124- \yii \base \Event::offAll ();
125- }
144+ Event::offAll ();
126145 Yii::setLogger (null );
127146 // This resolves an issue with database connections not closing properly.
128147 gc_collect_cycles ();
@@ -161,23 +180,23 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void
161180 * @param string $value The value of the cookie
162181 * @return string The value to send to the browser
163182 */
164- public function hashCookieData ($ name , $ value ): string
183+ public function hashCookieData (string $ name , string $ value ): string
165184 {
166- $ app = $ this ->getApplication ();
167- if (!$ app -> request ->enableCookieValidation ) {
185+ $ request = $ this ->getWebRequest ();
186+ if (!$ request ->enableCookieValidation ) {
168187 return $ value ;
169188 }
170- return $ app -> security ->hashData (serialize ([$ name , $ value ]), $ app -> request ->cookieValidationKey );
189+ return $ this -> getApplication ()-> security ->hashData (serialize ([$ name , $ value ]), $ request ->cookieValidationKey );
171190 }
172191
173192 /**
174193 * @internal
175- * @return array List of regex patterns for recognized domain names
194+ * @return non-empty-list<string> List of regex patterns for recognized domain names
176195 */
177196 public function getInternalDomains (): array
178197 {
179- /** @var \yii\web\UrlManager $urlManager */
180198 $ urlManager = $ this ->getApplication ()->urlManager ;
199+
181200 $ domains = [$ this ->getDomainRegex ($ urlManager ->hostInfo )];
182201 if ($ urlManager ->enablePrettyUrl ) {
183202 foreach ($ urlManager ->rules as $ rule ) {
@@ -187,12 +206,12 @@ public function getInternalDomains(): array
187206 }
188207 }
189208 }
190- return array_unique ($ domains );
209+ return array_values ( array_unique ($ domains) );
191210 }
192211
193212 /**
194213 * @internal
195- * @return array List of sent emails
214+ * @return list<BaseMessage> List of sent emails
196215 */
197216 public function getEmails (): array
198217 {
@@ -211,13 +230,14 @@ public function clearEmails(): void
211230 /**
212231 * @internal
213232 */
214- public function getComponent ($ name )
233+ public function getComponent (string $ name ): object | null
215234 {
216235 $ app = $ this ->getApplication ();
217- if (!$ app ->has ($ name )) {
236+ $ result = $ app ->get ($ name , false );
237+ if (!isset ($ result )) {
218238 throw new ConfigurationException ("Component $ name is not available in current application " );
219239 }
220- return $ app -> get ( $ name ) ;
240+ return $ result ;
221241 }
222242
223243 /**
@@ -240,6 +260,9 @@ function ($matches) use (&$parameters): string {
240260 $ template
241261 );
242262 }
263+ if ($ template === null ) {
264+ throw new \RuntimeException ("Failed to parse domain regex " );
265+ }
243266 $ template = preg_quote ($ template );
244267 $ template = strtr ($ template , $ parameters );
245268 return '/^ ' . $ template . '$/u ' ;
@@ -251,7 +274,7 @@ function ($matches) use (&$parameters): string {
251274 */
252275 public function getCsrfParamName (): string
253276 {
254- return $ this ->getApplication ()-> request ->csrfParam ;
277+ return $ this ->getWebRequest () ->csrfParam ;
255278 }
256279
257280 public function startApp (?\yii \log \Logger $ logger = null ): void
@@ -268,7 +291,11 @@ public function startApp(?\yii\log\Logger $logger = null): void
268291 }
269292
270293 $ config = $ this ->mockMailer ($ config );
271- Yii::$ app = Yii::createObject ($ config );
294+ $ app = Yii::createObject ($ config );
295+ if (!$ app instanceof \yii \base \Application) {
296+ throw new ModuleConfigException ($ this , "Failed to initialize Yii2 app " );
297+ }
298+ \Yii::$ app = $ app ;
272299
273300 if ($ logger instanceof \yii \log \Logger) {
274301 Yii::setLogger ($ logger );
@@ -278,9 +305,9 @@ public function startApp(?\yii\log\Logger $logger = null): void
278305 }
279306
280307 /**
281- * @param \Symfony\Component\BrowserKit\Request $request
308+ * @param BrowserkitRequest $request
282309 */
283- public function doRequest (object $ request ): \ Symfony \ Component \ BrowserKit \ Response
310+ public function doRequest (object $ request ): Response
284311 {
285312 $ _COOKIE = $ request ->getCookies ();
286313 $ _SERVER = $ request ->getServer ();
@@ -337,9 +364,9 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
337364 * Sending the response is problematic because it tries to send headers.
338365 */
339366 $ app ->trigger ($ app ::EVENT_BEFORE_REQUEST );
340- $ response = $ app ->handleRequest ($ yiiRequest );
367+ $ yiiResponse = $ app ->handleRequest ($ yiiRequest );
341368 $ app ->trigger ($ app ::EVENT_AFTER_REQUEST );
342- $ response ->send ();
369+ $ yiiResponse ->send ();
343370 } catch (\Exception $ e ) {
344371 if ($ e instanceof UserException) {
345372 // Don't discard output and pass exception handling to Yii to be able
@@ -350,37 +377,32 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
350377 // for exceptions not related to Http, we pass them to Codeception
351378 throw $ e ;
352379 }
353- $ response = $ app ->response ;
380+ $ yiiResponse = $ app ->response ;
354381 }
355382
356- $ this ->encodeCookies ($ response , $ yiiRequest , $ app ->security );
383+ $ this ->encodeCookies ($ yiiResponse , $ yiiRequest , $ app ->security );
357384
358- if ($ response ->isRedirection ) {
359- Debug::debug ("[Redirect with headers] " . print_r ($ response ->getHeaders ()->toArray (), true ));
385+ if ($ yiiResponse ->isRedirection ) {
386+ Debug::debug ("[Redirect with headers] " . print_r ($ yiiResponse ->getHeaders ()->toArray (), true ));
360387 }
361388
362389 $ content = ob_get_clean ();
363- if (empty ($ content ) && !empty ($ response ->content ) && !isset ($ response ->stream )) {
364- throw new \Exception ('No content was sent from Yii application ' );
390+ if (empty ($ content ) && !empty ($ yiiResponse ->content ) && !isset ($ yiiResponse ->stream )) {
391+ throw new \RuntimeException ('No content was sent from Yii application ' );
392+ } elseif ($ content === false ) {
393+ throw new \RuntimeException ('Failed to get output buffer ' );
365394 }
366395
367- return new Response ($ content , $ response ->statusCode , $ response ->getHeaders ()->toArray ());
368- }
369-
370- protected function revertErrorHandler ()
371- {
372- $ handler = new ErrorHandler ();
373- set_error_handler ([$ handler , 'errorHandler ' ]);
396+ return new Response ($ content , $ yiiResponse ->statusCode , $ yiiResponse ->getHeaders ()->toArray ());
374397 }
375398
376-
377399 /**
378400 * Encodes the cookies and adds them to the headers.
379401 * @throws \yii\base\InvalidConfigException
380402 */
381403 protected function encodeCookies (
382404 YiiResponse $ response ,
383- Request $ request ,
405+ YiiRequest $ request ,
384406 Security $ security
385407 ): void {
386408 if ($ request ->enableCookieValidation ) {
@@ -433,11 +455,19 @@ protected function mockMailer(array $config): array
433455
434456 $ mailerConfig = [
435457 'class ' => TestMailer::class,
436- 'callback ' => function (MessageInterface $ message ): void {
458+ 'callback ' => function (BaseMessage $ message ): void {
437459 $ this ->emails [] = $ message ;
438460 }
439461 ];
440462
463+ if (isset ($ config ['components ' ])) {
464+ if (!is_array ($ config ['components ' ])) {
465+ throw new ModuleConfigException ($ this ,
466+ "Yii2 config does not contain components key is not of type array " );
467+ }
468+ } else {
469+ $ config ['components ' ] = [];
470+ }
441471 if (isset ($ config ['components ' ]['mailer ' ]) && is_array ($ config ['components ' ]['mailer ' ])) {
442472 foreach ($ config ['components ' ]['mailer ' ] as $ name => $ value ) {
443473 if (in_array ($ name , $ allowedOptions , true )) {
@@ -487,7 +517,7 @@ public function setContext(array $context): void
487517 */
488518 public function closeSession (): void
489519 {
490- $ app = \Yii:: $ app ;
520+ $ app = $ this -> getApplication () ;
491521 if ($ app instanceof \yii \web \Application && $ app ->has ('session ' , true )) {
492522 $ app ->session ->close ();
493523 }
@@ -511,8 +541,8 @@ protected function resetResponse(Application $app): void
511541 Debug::debug (<<<TEXT
512542[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
513543the response object, this means any behaviors or events that are not attached in the component config will be lost.
514- We will fall back to clearing the response. If you are certain you want to recreate it, please configure
515- responseCleanMethod = 'force_recreate' in the module.
544+ We will fall back to clearing the response. If you are certain you want to recreate it, please configure
545+ responseCleanMethod = 'force_recreate' in the module.
516546TEXT
517547 );
518548 $ method = self ::CLEAN_CLEAR ;
0 commit comments