Skip to content

Commit afa7637

Browse files
committed
chore: Merge branch 'master' into mailer-proxy-alternative
2 parents 5bcf28e + 729e04d commit afa7637

File tree

9 files changed

+219
-131
lines changed

9 files changed

+219
-131
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88

99
strategy:
1010
matrix:
11-
php: [8.0, 8.1, 8.2, 8.3, 8.4]
11+
php: [8.3, 8.4]
1212

1313
steps:
1414
- name: Checkout code

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
],
1919
"minimum-stability": "RC",
2020
"require": {
21-
"php": "^8.0",
21+
"php": "^8.3",
2222
"codeception/codeception": "^5.0.8",
2323
"codeception/lib-innerbrowser": "^3.0 | ^4.0"
2424
},
@@ -29,8 +29,8 @@
2929
"codemix/yii2-localeurls": "^1.7",
3030
"codeception/module-asserts": ">= 3.0",
3131
"codeception/module-filesystem": "> 3.0",
32-
"phpstan/phpstan": "^1.10",
33-
"rector/rector": "^1.2"
32+
"phpstan/phpstan": "^2",
33+
"rector/rector": "^2"
3434
},
3535
"autoload":{
3636
"classmap": ["src/"]

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ parameters:
55
dynamicConstantNames:
66
- CONSOLE
77
- YII_DEBUG
8-
level: 5
8+
level: 9
99
paths:
1010
- src
1111
checkMaybeUndefinedVariables: true

src/Codeception/Lib/Connector/Yii2.php

Lines changed: 68 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Codeception\Lib\Connector;
66

77
use Codeception\Exception\ConfigurationException;
8+
use Codeception\Exception\ModuleConfigException;
89
use Codeception\Lib\Connector\Yii2\Logger;
910
use Codeception\Lib\Connector\Yii2\TestMailer;
1011
use Codeception\Util\Debug;
@@ -13,22 +14,28 @@
1314
use Symfony\Component\BrowserKit\Cookie;
1415
use Symfony\Component\BrowserKit\CookieJar;
1516
use Symfony\Component\BrowserKit\History;
17+
use Symfony\Component\BrowserKit\Request as BrowserkitRequest;
18+
use yii\web\Request as YiiRequest;
1619
use Symfony\Component\BrowserKit\Response;
1720
use Yii;
21+
use yii\base\Component;
22+
use yii\base\Event;
1823
use yii\base\ExitException;
1924
use yii\base\Security;
2025
use yii\base\UserException;
21-
use yii\mail\BaseMailer;
22-
use yii\mail\MailerInterface;
26+
use yii\mail\BaseMessage;
2327
use yii\mail\MailEvent;
24-
use yii\mail\MessageInterface;
2528
use yii\web\Application;
2629
use yii\web\ErrorHandler;
2730
use yii\web\IdentityInterface;
2831
use yii\web\Request;
2932
use yii\web\Response as YiiResponse;
3033
use yii\web\User;
3134

35+
36+
/**
37+
* @extends Client<BrowserkitRequest, Response>
38+
*/
3239
class 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
541556
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.
544559
TEXT
545560
);
546561
$method = self::CLEAN_CLEAR;

src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,15 @@ public function closeAll(): void
5858
}
5959
}
6060

61+
/**
62+
* @param string|array<mixed>|JsonSerializable $message
63+
* @return void
64+
*/
6165
protected function debug(string|array|JsonSerializable $message): void
6266
{
6367
$title = (new ReflectionClass($this))->getShortName();
6468
if (is_array($message) || is_object($message)) {
65-
$message = stripslashes(json_encode($message));
69+
$message = stripslashes(json_encode($message, JSON_THROW_ON_ERROR));
6670
}
6771
codecept_debug("[$title] $message");
6872
}

src/Codeception/Lib/Connector/Yii2/Logger.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@
1111

1212
class Logger extends YiiLogger
1313
{
14+
/**
15+
* @var \SplQueue<string>
16+
*/
1417
private \SplQueue $logQueue;
1518

16-
public function __construct(private int $maxLogItems = 5, array $config = [])
19+
/**
20+
* @param int $maxLogItems
21+
* @param array<string, mixed> $config
22+
*/
23+
public function __construct(private readonly int $maxLogItems = 5, array $config = [])
1724
{
1825
parent::__construct($config);
1926
$this->logQueue = new \SplQueue();
@@ -25,7 +32,7 @@ public function init(): void
2532
}
2633

2734
/**
28-
* @param string|array|YiiException $message
35+
* @param string|array<mixed>|YiiException $message
2936
* @param self::LEVEL_INFO|self::LEVEL_WARNING|self::LEVEL_ERROR $level
3037
* @param string $category
3138
*/

0 commit comments

Comments
 (0)