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 ;
1618use Symfony \Component \BrowserKit \Response ;
1719use Yii ;
20+ use yii \base \Component ;
21+ use yii \base \Event ;
1822use yii \base \ExitException ;
1923use yii \base \Security ;
2024use yii \base \UserException ;
25+ use yii \mail \BaseMessage ;
2126use yii \mail \MessageInterface ;
2227use yii \web \Application ;
2328use yii \web \ErrorHandler ;
2631use yii \web \Response as YiiResponse ;
2732use yii \web \User ;
2833
34+
35+ /**
36+ * @extends Client<BrowserkitRequest, Response>
37+ */
2938class Yii2 extends Client
3039{
3140 use Shared \PhpSuperGlobalsConverter;
@@ -98,18 +107,29 @@ class Yii2 extends Client
98107 public string |null $ applicationClass = null ;
99108
100109
110+ /**
111+ * @var list<BaseMessage>
112+ */
101113 private array $ emails = [];
102114
103115 /**
104- * @deprecated since 2.5, will become protected in 3.0. Directly access to \Yii::$app if you need to interact with it.
105116 * @internal
106117 */
107- public function getApplication (): \yii \base \Application
118+ protected function getApplication (): \yii \base \Application
108119 {
109120 if (!isset (Yii::$ app )) {
110121 $ this ->startApp ();
111122 }
112- return Yii::$ app ;
123+ return Yii::$ app ?? throw new \RuntimeException ('Failed to create Yii2 application ' );
124+ }
125+
126+ private function getWebRequest (): Request
127+ {
128+ $ request = $ this ->getApplication ()->request ;
129+ if (!$ request instanceof Request) {
130+ throw new \RuntimeException ('Request component is not of type ' . Request::class);
131+ }
132+ return $ request ;
113133 }
114134
115135 public function resetApplication (bool $ closeSession = true ): void
@@ -120,9 +140,7 @@ public function resetApplication(bool $closeSession = true): void
120140 }
121141 Yii::$ app = null ;
122142 \yii \web \UploadedFile::reset ();
123- if (method_exists (\yii \base \Event::class, 'offAll ' )) {
124- \yii \base \Event::offAll ();
125- }
143+ Event::offAll ();
126144 Yii::setLogger (null );
127145 // This resolves an issue with database connections not closing properly.
128146 gc_collect_cycles ();
@@ -161,23 +179,27 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void
161179 * @param string $value The value of the cookie
162180 * @return string The value to send to the browser
163181 */
164- public function hashCookieData ($ name , $ value ): string
182+ public function hashCookieData (string $ name , string $ value ): string
165183 {
166184 $ app = $ this ->getApplication ();
167- if (!$ app ->request ->enableCookieValidation ) {
185+ $ request = $ app ->getRequest ();
186+ if (!$ request instanceof Request) {
187+ throw new \RuntimeException ("Can't do cookie operations on non-web requests " );
188+ }
189+ if (!$ request ->enableCookieValidation ) {
168190 return $ value ;
169191 }
170- return $ app ->security ->hashData (serialize ([$ name , $ value ]), $ app -> request ->cookieValidationKey );
192+ return $ app ->security ->hashData (serialize ([$ name , $ value ]), $ request ->cookieValidationKey );
171193 }
172194
173195 /**
174196 * @internal
175- * @return array List of regex patterns for recognized domain names
197+ * @return non-empty-list<string> List of regex patterns for recognized domain names
176198 */
177199 public function getInternalDomains (): array
178200 {
179- /** @var \yii\web\UrlManager $urlManager */
180201 $ urlManager = $ this ->getApplication ()->urlManager ;
202+
181203 $ domains = [$ this ->getDomainRegex ($ urlManager ->hostInfo )];
182204 if ($ urlManager ->enablePrettyUrl ) {
183205 foreach ($ urlManager ->rules as $ rule ) {
@@ -187,12 +209,12 @@ public function getInternalDomains(): array
187209 }
188210 }
189211 }
190- return array_unique ($ domains );
212+ return array_values ( array_unique ($ domains) );
191213 }
192214
193215 /**
194216 * @internal
195- * @return array List of sent emails
217+ * @return list<BaseMessage> List of sent emails
196218 */
197219 public function getEmails (): array
198220 {
@@ -211,13 +233,14 @@ public function clearEmails(): void
211233 /**
212234 * @internal
213235 */
214- public function getComponent ($ name )
236+ public function getComponent (string $ name ): object | null
215237 {
216238 $ app = $ this ->getApplication ();
217- if (!$ app ->has ($ name )) {
239+ $ result = $ app ->get ($ name , false );
240+ if (!isset ($ result )) {
218241 throw new ConfigurationException ("Component $ name is not available in current application " );
219242 }
220- return $ app -> get ( $ name ) ;
243+ return $ result ;
221244 }
222245
223246 /**
@@ -240,6 +263,9 @@ function ($matches) use (&$parameters): string {
240263 $ template
241264 );
242265 }
266+ if ($ template === null ) {
267+ throw new \RuntimeException ("Failed to parse domain regex " );
268+ }
243269 $ template = preg_quote ($ template );
244270 $ template = strtr ($ template , $ parameters );
245271 return '/^ ' . $ template . '$/u ' ;
@@ -251,7 +277,7 @@ function ($matches) use (&$parameters): string {
251277 */
252278 public function getCsrfParamName (): string
253279 {
254- return $ this ->getApplication ()-> request ->csrfParam ;
280+ return $ this ->getWebRequest () ->csrfParam ;
255281 }
256282
257283 public function startApp (?\yii \log \Logger $ logger = null ): void
@@ -268,7 +294,11 @@ public function startApp(?\yii\log\Logger $logger = null): void
268294 }
269295
270296 $ config = $ this ->mockMailer ($ config );
271- Yii::$ app = Yii::createObject ($ config );
297+ $ app = Yii::createObject ($ config );
298+ if (!$ app instanceof \yii \base \Application) {
299+ throw new ModuleConfigException ($ this , "Failed to initialize Yii2 app " );
300+ }
301+ \Yii::$ app = $ app ;
272302
273303 if ($ logger instanceof \yii \log \Logger) {
274304 Yii::setLogger ($ logger );
@@ -278,9 +308,9 @@ public function startApp(?\yii\log\Logger $logger = null): void
278308 }
279309
280310 /**
281- * @param \Symfony\Component\BrowserKit\Request $request
311+ * @param BrowserkitRequest $request
282312 */
283- public function doRequest (object $ request ): \ Symfony \ Component \ BrowserKit \ Response
313+ public function doRequest (object $ request ): Response
284314 {
285315 $ _COOKIE = $ request ->getCookies ();
286316 $ _SERVER = $ request ->getServer ();
@@ -362,18 +392,13 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon
362392 $ content = ob_get_clean ();
363393 if (empty ($ content ) && !empty ($ response ->content ) && !isset ($ response ->stream )) {
364394 throw new \Exception ('No content was sent from Yii application ' );
395+ } elseif ($ content === false ) {
396+ throw new \Exception ('Failed to get output buffer ' );
365397 }
366398
367399 return new Response ($ content , $ response ->statusCode , $ response ->getHeaders ()->toArray ());
368400 }
369401
370- protected function revertErrorHandler ()
371- {
372- $ handler = new ErrorHandler ();
373- set_error_handler ([$ handler , 'errorHandler ' ]);
374- }
375-
376-
377402 /**
378403 * Encodes the cookies and adds them to the headers.
379404 * @throws \yii\base\InvalidConfigException
@@ -433,11 +458,19 @@ protected function mockMailer(array $config): array
433458
434459 $ mailerConfig = [
435460 'class ' => TestMailer::class,
436- 'callback ' => function (MessageInterface $ message ): void {
461+ 'callback ' => function (BaseMessage $ message ): void {
437462 $ this ->emails [] = $ message ;
438463 }
439464 ];
440465
466+ if (isset ($ config ['components ' ])) {
467+ if (!is_array ($ config ['components ' ])) {
468+ throw new ModuleConfigException ($ this ,
469+ "Yii2 config does not contain components key is not of type array " );
470+ }
471+ } else {
472+ $ config ['components ' ] = [];
473+ }
441474 if (isset ($ config ['components ' ]['mailer ' ]) && is_array ($ config ['components ' ]['mailer ' ])) {
442475 foreach ($ config ['components ' ]['mailer ' ] as $ name => $ value ) {
443476 if (in_array ($ name , $ allowedOptions , true )) {
@@ -511,8 +544,8 @@ protected function resetResponse(Application $app): void
511544 Debug::debug (<<<TEXT
512545[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
513546the 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.
547+ We will fall back to clearing the response. If you are certain you want to recreate it, please configure
548+ responseCleanMethod = 'force_recreate' in the module.
516549TEXT
517550 );
518551 $ method = self ::CLEAN_CLEAR ;
0 commit comments