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 ;
21- use yii \mail \BaseMailer ;
22- use yii \mail \MailerInterface ;
26+ use yii \mail \BaseMessage ;
2327use yii \mail \MailEvent ;
24- use yii \mail \MessageInterface ;
2528use yii \web \Application ;
2629use yii \web \ErrorHandler ;
2730use yii \web \IdentityInterface ;
2831use yii \web \Request ;
2932use yii \web \Response as YiiResponse ;
3033use yii \web \User ;
3134
35+
36+ /**
37+ * @extends Client<BrowserkitRequest, Response>
38+ */
3239class Yii2 extends Client
3340{
3441 use Shared \PhpSuperGlobalsConverter;
@@ -118,18 +125,29 @@ class Yii2 extends Client
118125 public string |null $ applicationClass = null ;
119126
120127
128+ /**
129+ * @var list<BaseMessage>
130+ */
121131 private array $ emails = [];
122132
123133 /**
124- * @deprecated since 2.5, will become protected in 3.0. Directly access to \Yii::$app if you need to interact with it.
125134 * @internal
126135 */
127- public function getApplication (): \yii \base \Application
136+ protected function getApplication (): \yii \base \Application
128137 {
129138 if (!isset (Yii::$ app )) {
130139 $ this ->startApp ();
131140 }
132- return Yii::$ app ;
141+ return Yii::$ app ?? throw new \RuntimeException ('Failed to create Yii2 application ' );
142+ }
143+
144+ private function getWebRequest (): YiiRequest
145+ {
146+ $ request = $ this ->getApplication ()->request ;
147+ if (!$ request instanceof YiiRequest) {
148+ throw new \RuntimeException ('Request component is not of type ' . YiiRequest::class);
149+ }
150+ return $ request ;
133151 }
134152
135153 public function resetApplication (bool $ closeSession = true ): void
@@ -140,9 +158,7 @@ public function resetApplication(bool $closeSession = true): void
140158 }
141159 Yii::$ app = null ;
142160 \yii \web \UploadedFile::reset ();
143- if (method_exists (\yii \base \Event::class, 'offAll ' )) {
144- \yii \base \Event::offAll ();
145- }
161+ Event::offAll ();
146162 Yii::setLogger (null );
147163 // This resolves an issue with database connections not closing properly.
148164 gc_collect_cycles ();
@@ -181,23 +197,23 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void
181197 * @param string $value The value of the cookie
182198 * @return string The value to send to the browser
183199 */
184- public function hashCookieData ($ name , $ value ): string
200+ public function hashCookieData (string $ name , string $ value ): string
185201 {
186- $ app = $ this ->getApplication ();
187- if (!$ app -> request ->enableCookieValidation ) {
202+ $ request = $ this ->getWebRequest ();
203+ if (!$ request ->enableCookieValidation ) {
188204 return $ value ;
189205 }
190- return $ app -> security ->hashData (serialize ([$ name , $ value ]), $ app -> request ->cookieValidationKey );
206+ return $ this -> getApplication ()-> security ->hashData (serialize ([$ name , $ value ]), $ request ->cookieValidationKey );
191207 }
192208
193209 /**
194210 * @internal
195- * @return array List of regex patterns for recognized domain names
211+ * @return non-empty-list<string> List of regex patterns for recognized domain names
196212 */
197213 public function getInternalDomains (): array
198214 {
199- /** @var \yii\web\UrlManager $urlManager */
200215 $ urlManager = $ this ->getApplication ()->urlManager ;
216+
201217 $ domains = [$ this ->getDomainRegex ($ urlManager ->hostInfo )];
202218 if ($ urlManager ->enablePrettyUrl ) {
203219 foreach ($ urlManager ->rules as $ rule ) {
@@ -207,12 +223,12 @@ public function getInternalDomains(): array
207223 }
208224 }
209225 }
210- return array_unique ($ domains );
226+ return array_values ( array_unique ($ domains) );
211227 }
212228
213229 /**
214230 * @internal
215- * @return array List of sent emails
231+ * @return list<BaseMessage> List of sent emails
216232 */
217233 public function getEmails (): array
218234 {
@@ -231,13 +247,14 @@ public function clearEmails(): void
231247 /**
232248 * @internal
233249 */
234- public function getComponent ($ name )
250+ public function getComponent (string $ name ): object | null
235251 {
236252 $ app = $ this ->getApplication ();
237- if (!$ app ->has ($ name )) {
253+ $ result = $ app ->get ($ name , false );
254+ if (!isset ($ result )) {
238255 throw new ConfigurationException ("Component $ name is not available in current application " );
239256 }
240- return $ app -> get ( $ name ) ;
257+ return $ result ;
241258 }
242259
243260 /**
@@ -260,6 +277,9 @@ function ($matches) use (&$parameters): string {
260277 $ template
261278 );
262279 }
280+ if ($ template === null ) {
281+ throw new \RuntimeException ("Failed to parse domain regex " );
282+ }
263283 $ template = preg_quote ($ template );
264284 $ template = strtr ($ template , $ parameters );
265285 return '/^ ' . $ template . '$/u ' ;
@@ -271,7 +291,7 @@ function ($matches) use (&$parameters): string {
271291 */
272292 public function getCsrfParamName (): string
273293 {
274- return $ this ->getApplication ()-> request ->csrfParam ;
294+ return $ this ->getWebRequest () ->csrfParam ;
275295 }
276296
277297 public function startApp (?\yii \log \Logger $ logger = null ): void
@@ -287,15 +307,7 @@ public function startApp(?\yii\log\Logger $logger = null): void
287307 unset($ config ['container ' ]);
288308 }
289309
290- match ($ this ->mailMethod ) {
291- self ::MAIL_CATCH => $ config = $ this ->mockMailer ($ config ),
292- self ::MAIL_EVENT_AFTER => $ config ['components ' ]['mailer ' ]['on ' . BaseMailer::EVENT_AFTER_SEND ] = fn (MailEvent $ event ) => $ this ->emails [] = $ event ->message ,
293- self ::MAIL_EVENT_BEFORE => $ config ['components ' ]['mailer ' ]['on ' . BaseMailer::EVENT_BEFORE_SEND ] = function (MailEvent $ event ) {
294- $ this ->emails [] = $ event ->message ;
295- return true ;
296- },
297- self ::MAIL_IGNORE => null // Do nothing
298- };
310+ $ config = $ this ->mockMailer ($ config );
299311 Yii::$ app = Yii::createObject ($ config );
300312
301313 if ($ logger instanceof \yii \log \Logger) {
@@ -306,9 +318,9 @@ public function startApp(?\yii\log\Logger $logger = null): void
306318 }
307319
308320 /**
309- * @param \Symfony\Component\BrowserKit\Request $request
321+ * @param BrowserkitRequest $request
310322 */
311- public function doRequest (object $ request ): \ Symfony \ Component \ BrowserKit \ Response
323+ public function doRequest (object $ request ): Response
312324 {
313325 $ _COOKIE = $ request ->getCookies ();
314326 $ _SERVER = $ request ->getServer ();
@@ -365,9 +377,9 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
365377 * Sending the response is problematic because it tries to send headers.
366378 */
367379 $ app ->trigger ($ app ::EVENT_BEFORE_REQUEST );
368- $ response = $ app ->handleRequest ($ yiiRequest );
380+ $ yiiResponse = $ app ->handleRequest ($ yiiRequest );
369381 $ app ->trigger ($ app ::EVENT_AFTER_REQUEST );
370- $ response ->send ();
382+ $ yiiResponse ->send ();
371383 } catch (\Exception $ e ) {
372384 if ($ e instanceof UserException) {
373385 // Don't discard output and pass exception handling to Yii to be able
@@ -378,37 +390,32 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
378390 // for exceptions not related to Http, we pass them to Codeception
379391 throw $ e ;
380392 }
381- $ response = $ app ->response ;
393+ $ yiiResponse = $ app ->response ;
382394 }
383395
384- $ this ->encodeCookies ($ response , $ yiiRequest , $ app ->security );
396+ $ this ->encodeCookies ($ yiiResponse , $ yiiRequest , $ app ->security );
385397
386- if ($ response ->isRedirection ) {
387- Debug::debug ("[Redirect with headers] " . print_r ($ response ->getHeaders ()->toArray (), true ));
398+ if ($ yiiResponse ->isRedirection ) {
399+ Debug::debug ("[Redirect with headers] " . print_r ($ yiiResponse ->getHeaders ()->toArray (), true ));
388400 }
389401
390402 $ content = ob_get_clean ();
391- if (empty ($ content ) && !empty ($ response ->content ) && !isset ($ response ->stream )) {
392- throw new \Exception ('No content was sent from Yii application ' );
403+ if (empty ($ content ) && !empty ($ yiiResponse ->content ) && !isset ($ yiiResponse ->stream )) {
404+ throw new \RuntimeException ('No content was sent from Yii application ' );
405+ } elseif ($ content === false ) {
406+ throw new \RuntimeException ('Failed to get output buffer ' );
393407 }
394408
395- return new Response ($ content , $ response ->statusCode , $ response ->getHeaders ()->toArray ());
396- }
397-
398- protected function revertErrorHandler ()
399- {
400- $ handler = new ErrorHandler ();
401- set_error_handler ([$ handler , 'errorHandler ' ]);
409+ return new Response ($ content , $ yiiResponse ->statusCode , $ yiiResponse ->getHeaders ()->toArray ());
402410 }
403411
404-
405412 /**
406413 * Encodes the cookies and adds them to the headers.
407414 * @throws \yii\base\InvalidConfigException
408415 */
409416 protected function encodeCookies (
410417 YiiResponse $ response ,
411- Request $ request ,
418+ YiiRequest $ request ,
412419 Security $ security
413420 ): void {
414421 if ($ request ->enableCookieValidation ) {
@@ -461,11 +468,19 @@ protected function mockMailer(array $config): array
461468
462469 $ mailerConfig = [
463470 'class ' => TestMailer::class,
464- 'callback ' => function (MessageInterface $ message ): void {
471+ 'callback ' => function (BaseMessage $ message ): void {
465472 $ this ->emails [] = $ message ;
466473 }
467474 ];
468475
476+ if (isset ($ config ['components ' ])) {
477+ if (!is_array ($ config ['components ' ])) {
478+ throw new ModuleConfigException ($ this ,
479+ "Yii2 config does not contain components key is not of type array " );
480+ }
481+ } else {
482+ $ config ['components ' ] = [];
483+ }
469484 if (isset ($ config ['components ' ]['mailer ' ]) && is_array ($ config ['components ' ]['mailer ' ])) {
470485 foreach ($ config ['components ' ]['mailer ' ] as $ name => $ value ) {
471486 if (in_array ($ name , $ allowedOptions , true )) {
@@ -515,7 +530,7 @@ public function setContext(array $context): void
515530 */
516531 public function closeSession (): void
517532 {
518- $ app = \Yii:: $ app ;
533+ $ app = $ this -> getApplication () ;
519534 if ($ app instanceof \yii \web \Application && $ app ->has ('session ' , true )) {
520535 $ app ->session ->close ();
521536 }
@@ -539,8 +554,8 @@ protected function resetResponse(Application $app): void
539554 Debug::debug (<<<TEXT
540555[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
541556the response object, this means any behaviors or events that are not attached in the component config will be lost.
542- We will fall back to clearing the response. If you are certain you want to recreate it, please configure
543- responseCleanMethod = 'force_recreate' in the module.
557+ We will fall back to clearing the response. If you are certain you want to recreate it, please configure
558+ responseCleanMethod = 'force_recreate' in the module.
544559TEXT
545560 );
546561 $ method = self ::CLEAN_CLEAR ;
0 commit comments