5
5
namespace Codeception \Lib \Connector ;
6
6
7
7
use Codeception \Exception \ConfigurationException ;
8
+ use Codeception \Exception \ModuleConfigException ;
8
9
use Codeception \Lib \Connector \Yii2 \Logger ;
9
10
use Codeception \Lib \Connector \Yii2 \TestMailer ;
10
11
use Codeception \Util \Debug ;
13
14
use Symfony \Component \BrowserKit \Cookie ;
14
15
use Symfony \Component \BrowserKit \CookieJar ;
15
16
use Symfony \Component \BrowserKit \History ;
17
+ use Symfony \Component \BrowserKit \Request as BrowserkitRequest ;
18
+ use yii \web \Request as YiiRequest ;
16
19
use Symfony \Component \BrowserKit \Response ;
17
20
use Yii ;
21
+ use yii \base \Component ;
22
+ use yii \base \Event ;
18
23
use yii \base \ExitException ;
19
24
use yii \base \Security ;
20
25
use yii \base \UserException ;
21
- use yii \mail \BaseMailer ;
22
- use yii \mail \MailerInterface ;
26
+ use yii \mail \BaseMessage ;
23
27
use yii \mail \MailEvent ;
24
- use yii \mail \MessageInterface ;
25
28
use yii \web \Application ;
26
29
use yii \web \ErrorHandler ;
27
30
use yii \web \IdentityInterface ;
28
31
use yii \web \Request ;
29
32
use yii \web \Response as YiiResponse ;
30
33
use yii \web \User ;
31
34
35
+
36
+ /**
37
+ * @extends Client<BrowserkitRequest, Response>
38
+ */
32
39
class Yii2 extends Client
33
40
{
34
41
use Shared \PhpSuperGlobalsConverter;
@@ -118,18 +125,29 @@ class Yii2 extends Client
118
125
public string |null $ applicationClass = null ;
119
126
120
127
128
+ /**
129
+ * @var list<BaseMessage>
130
+ */
121
131
private array $ emails = [];
122
132
123
133
/**
124
- * @deprecated since 2.5, will become protected in 3.0. Directly access to \Yii::$app if you need to interact with it.
125
134
* @internal
126
135
*/
127
- public function getApplication (): \yii \base \Application
136
+ protected function getApplication (): \yii \base \Application
128
137
{
129
138
if (!isset (Yii::$ app )) {
130
139
$ this ->startApp ();
131
140
}
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 ;
133
151
}
134
152
135
153
public function resetApplication (bool $ closeSession = true ): void
@@ -140,9 +158,7 @@ public function resetApplication(bool $closeSession = true): void
140
158
}
141
159
Yii::$ app = null ;
142
160
\yii \web \UploadedFile::reset ();
143
- if (method_exists (\yii \base \Event::class, 'offAll ' )) {
144
- \yii \base \Event::offAll ();
145
- }
161
+ Event::offAll ();
146
162
Yii::setLogger (null );
147
163
// This resolves an issue with database connections not closing properly.
148
164
gc_collect_cycles ();
@@ -181,23 +197,23 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void
181
197
* @param string $value The value of the cookie
182
198
* @return string The value to send to the browser
183
199
*/
184
- public function hashCookieData ($ name , $ value ): string
200
+ public function hashCookieData (string $ name , string $ value ): string
185
201
{
186
- $ app = $ this ->getApplication ();
187
- if (!$ app -> request ->enableCookieValidation ) {
202
+ $ request = $ this ->getWebRequest ();
203
+ if (!$ request ->enableCookieValidation ) {
188
204
return $ value ;
189
205
}
190
- return $ app -> security ->hashData (serialize ([$ name , $ value ]), $ app -> request ->cookieValidationKey );
206
+ return $ this -> getApplication ()-> security ->hashData (serialize ([$ name , $ value ]), $ request ->cookieValidationKey );
191
207
}
192
208
193
209
/**
194
210
* @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
196
212
*/
197
213
public function getInternalDomains (): array
198
214
{
199
- /** @var \yii\web\UrlManager $urlManager */
200
215
$ urlManager = $ this ->getApplication ()->urlManager ;
216
+
201
217
$ domains = [$ this ->getDomainRegex ($ urlManager ->hostInfo )];
202
218
if ($ urlManager ->enablePrettyUrl ) {
203
219
foreach ($ urlManager ->rules as $ rule ) {
@@ -207,12 +223,12 @@ public function getInternalDomains(): array
207
223
}
208
224
}
209
225
}
210
- return array_unique ($ domains );
226
+ return array_values ( array_unique ($ domains) );
211
227
}
212
228
213
229
/**
214
230
* @internal
215
- * @return array List of sent emails
231
+ * @return list<BaseMessage> List of sent emails
216
232
*/
217
233
public function getEmails (): array
218
234
{
@@ -231,13 +247,14 @@ public function clearEmails(): void
231
247
/**
232
248
* @internal
233
249
*/
234
- public function getComponent ($ name )
250
+ public function getComponent (string $ name ): object | null
235
251
{
236
252
$ app = $ this ->getApplication ();
237
- if (!$ app ->has ($ name )) {
253
+ $ result = $ app ->get ($ name , false );
254
+ if (!isset ($ result )) {
238
255
throw new ConfigurationException ("Component $ name is not available in current application " );
239
256
}
240
- return $ app -> get ( $ name ) ;
257
+ return $ result ;
241
258
}
242
259
243
260
/**
@@ -260,6 +277,9 @@ function ($matches) use (&$parameters): string {
260
277
$ template
261
278
);
262
279
}
280
+ if ($ template === null ) {
281
+ throw new \RuntimeException ("Failed to parse domain regex " );
282
+ }
263
283
$ template = preg_quote ($ template );
264
284
$ template = strtr ($ template , $ parameters );
265
285
return '/^ ' . $ template . '$/u ' ;
@@ -271,7 +291,7 @@ function ($matches) use (&$parameters): string {
271
291
*/
272
292
public function getCsrfParamName (): string
273
293
{
274
- return $ this ->getApplication ()-> request ->csrfParam ;
294
+ return $ this ->getWebRequest () ->csrfParam ;
275
295
}
276
296
277
297
public function startApp (?\yii \log \Logger $ logger = null ): void
@@ -287,15 +307,7 @@ public function startApp(?\yii\log\Logger $logger = null): void
287
307
unset($ config ['container ' ]);
288
308
}
289
309
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 );
299
311
Yii::$ app = Yii::createObject ($ config );
300
312
301
313
if ($ logger instanceof \yii \log \Logger) {
@@ -306,9 +318,9 @@ public function startApp(?\yii\log\Logger $logger = null): void
306
318
}
307
319
308
320
/**
309
- * @param \Symfony\Component\BrowserKit\Request $request
321
+ * @param BrowserkitRequest $request
310
322
*/
311
- public function doRequest (object $ request ): \ Symfony \ Component \ BrowserKit \ Response
323
+ public function doRequest (object $ request ): Response
312
324
{
313
325
$ _COOKIE = $ request ->getCookies ();
314
326
$ _SERVER = $ request ->getServer ();
@@ -365,9 +377,9 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
365
377
* Sending the response is problematic because it tries to send headers.
366
378
*/
367
379
$ app ->trigger ($ app ::EVENT_BEFORE_REQUEST );
368
- $ response = $ app ->handleRequest ($ yiiRequest );
380
+ $ yiiResponse = $ app ->handleRequest ($ yiiRequest );
369
381
$ app ->trigger ($ app ::EVENT_AFTER_REQUEST );
370
- $ response ->send ();
382
+ $ yiiResponse ->send ();
371
383
} catch (\Exception $ e ) {
372
384
if ($ e instanceof UserException) {
373
385
// 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
378
390
// for exceptions not related to Http, we pass them to Codeception
379
391
throw $ e ;
380
392
}
381
- $ response = $ app ->response ;
393
+ $ yiiResponse = $ app ->response ;
382
394
}
383
395
384
- $ this ->encodeCookies ($ response , $ yiiRequest , $ app ->security );
396
+ $ this ->encodeCookies ($ yiiResponse , $ yiiRequest , $ app ->security );
385
397
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 ));
388
400
}
389
401
390
402
$ 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 ' );
393
407
}
394
408
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 ());
402
410
}
403
411
404
-
405
412
/**
406
413
* Encodes the cookies and adds them to the headers.
407
414
* @throws \yii\base\InvalidConfigException
408
415
*/
409
416
protected function encodeCookies (
410
417
YiiResponse $ response ,
411
- Request $ request ,
418
+ YiiRequest $ request ,
412
419
Security $ security
413
420
): void {
414
421
if ($ request ->enableCookieValidation ) {
@@ -461,11 +468,19 @@ protected function mockMailer(array $config): array
461
468
462
469
$ mailerConfig = [
463
470
'class ' => TestMailer::class,
464
- 'callback ' => function (MessageInterface $ message ): void {
471
+ 'callback ' => function (BaseMessage $ message ): void {
465
472
$ this ->emails [] = $ message ;
466
473
}
467
474
];
468
475
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
+ }
469
484
if (isset ($ config ['components ' ]['mailer ' ]) && is_array ($ config ['components ' ]['mailer ' ])) {
470
485
foreach ($ config ['components ' ]['mailer ' ] as $ name => $ value ) {
471
486
if (in_array ($ name , $ allowedOptions , true )) {
@@ -515,7 +530,7 @@ public function setContext(array $context): void
515
530
*/
516
531
public function closeSession (): void
517
532
{
518
- $ app = \Yii:: $ app ;
533
+ $ app = $ this -> getApplication () ;
519
534
if ($ app instanceof \yii \web \Application && $ app ->has ('session ' , true )) {
520
535
$ app ->session ->close ();
521
536
}
@@ -539,8 +554,8 @@ protected function resetResponse(Application $app): void
539
554
Debug::debug (<<<TEXT
540
555
[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
541
556
the 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.
544
559
TEXT
545
560
);
546
561
$ method = self ::CLEAN_CLEAR ;
0 commit comments