From 0b08f6481a93807dd7e9ce8b8ab22e937b25e923 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 10:25:10 +0100 Subject: [PATCH 1/8] feat: drop php8.1 php8.2 --- composer.json | 6 +++--- phpstan.neon | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8cb1f78..f93903b 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "minimum-stability": "RC", "require": { - "php": "^8.0", + "php": "^8.3", "codeception/codeception": "^5.0.8", "codeception/lib-innerbrowser": "^3.0 | ^4.0" }, @@ -29,8 +29,8 @@ "codemix/yii2-localeurls": "^1.7", "codeception/module-asserts": ">= 3.0", "codeception/module-filesystem": "> 3.0", - "phpstan/phpstan": "^1.10", - "rector/rector": "^1.2" + "phpstan/phpstan": "^2", + "rector/rector": "^2" }, "autoload":{ "classmap": ["src/"] diff --git a/phpstan.neon b/phpstan.neon index 7019dd5..9b7e02f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,7 +5,7 @@ parameters: dynamicConstantNames: - CONSOLE - YII_DEBUG - level: 5 + level: 9 paths: - src checkMaybeUndefinedVariables: true From baba62281f6839e1c21e646d50971156fd7b8faa Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 11:48:31 +0100 Subject: [PATCH 2/8] chore(cs): fix all phpstan errors, increase level --- src/Codeception/Lib/Connector/Yii2.php | 93 +++++++---- .../Lib/Connector/Yii2/ConnectionWatcher.php | 6 +- src/Codeception/Lib/Connector/Yii2/Logger.php | 11 +- .../Lib/Connector/Yii2/TransactionForcer.php | 13 +- src/Codeception/Module/Yii2.php | 157 +++++++++++++----- 5 files changed, 203 insertions(+), 77 deletions(-) diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index b7df1d5..d9ff2cd 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -5,6 +5,7 @@ namespace Codeception\Lib\Connector; use Codeception\Exception\ConfigurationException; +use Codeception\Exception\ModuleConfigException; use Codeception\Lib\Connector\Yii2\Logger; use Codeception\Lib\Connector\Yii2\TestMailer; use Codeception\Util\Debug; @@ -13,11 +14,15 @@ use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; +use Symfony\Component\BrowserKit\Request as BrowserkitRequest; use Symfony\Component\BrowserKit\Response; use Yii; +use yii\base\Component; +use yii\base\Event; use yii\base\ExitException; use yii\base\Security; use yii\base\UserException; +use yii\mail\BaseMessage; use yii\mail\MessageInterface; use yii\web\Application; use yii\web\ErrorHandler; @@ -26,6 +31,10 @@ use yii\web\Response as YiiResponse; use yii\web\User; + +/** + * @extends Client + */ class Yii2 extends Client { use Shared\PhpSuperGlobalsConverter; @@ -98,18 +107,29 @@ class Yii2 extends Client public string|null $applicationClass = null; + /** + * @var list + */ private array $emails = []; /** - * @deprecated since 2.5, will become protected in 3.0. Directly access to \Yii::$app if you need to interact with it. * @internal */ - public function getApplication(): \yii\base\Application + protected function getApplication(): \yii\base\Application { if (!isset(Yii::$app)) { $this->startApp(); } - return Yii::$app; + return Yii::$app ?? throw new \RuntimeException('Failed to create Yii2 application'); + } + + private function getWebRequest(): Request + { + $request = $this->getApplication()->request; + if (!$request instanceof Request) { + throw new \RuntimeException('Request component is not of type ' . Request::class); + } + return $request; } public function resetApplication(bool $closeSession = true): void @@ -120,9 +140,7 @@ public function resetApplication(bool $closeSession = true): void } Yii::$app = null; \yii\web\UploadedFile::reset(); - if (method_exists(\yii\base\Event::class, 'offAll')) { - \yii\base\Event::offAll(); - } + Event::offAll(); Yii::setLogger(null); // This resolves an issue with database connections not closing properly. gc_collect_cycles(); @@ -161,23 +179,27 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void * @param string $value The value of the cookie * @return string The value to send to the browser */ - public function hashCookieData($name, $value): string + public function hashCookieData(string $name, string $value): string { $app = $this->getApplication(); - if (!$app->request->enableCookieValidation) { + $request = $app->getRequest(); + if (!$request instanceof Request) { + throw new \RuntimeException("Can't do cookie operations on non-web requests"); + } + if (!$request->enableCookieValidation) { return $value; } - return $app->security->hashData(serialize([$name, $value]), $app->request->cookieValidationKey); + return $app->security->hashData(serialize([$name, $value]), $request->cookieValidationKey); } /** * @internal - * @return array List of regex patterns for recognized domain names + * @return non-empty-list List of regex patterns for recognized domain names */ public function getInternalDomains(): array { - /** @var \yii\web\UrlManager $urlManager */ $urlManager = $this->getApplication()->urlManager; + $domains = [$this->getDomainRegex($urlManager->hostInfo)]; if ($urlManager->enablePrettyUrl) { foreach ($urlManager->rules as $rule) { @@ -187,12 +209,12 @@ public function getInternalDomains(): array } } } - return array_unique($domains); + return array_values(array_unique($domains)); } /** * @internal - * @return array List of sent emails + * @return list List of sent emails */ public function getEmails(): array { @@ -211,13 +233,14 @@ public function clearEmails(): void /** * @internal */ - public function getComponent($name) + public function getComponent(string $name): object|null { $app = $this->getApplication(); - if (!$app->has($name)) { + $result = $app->get($name, false); + if (!isset($result)) { throw new ConfigurationException("Component $name is not available in current application"); } - return $app->get($name); + return $result; } /** @@ -240,6 +263,9 @@ function ($matches) use (&$parameters): string { $template ); } + if ($template === null) { + throw new \RuntimeException("Failed to parse domain regex"); + } $template = preg_quote($template); $template = strtr($template, $parameters); return '/^' . $template . '$/u'; @@ -251,7 +277,7 @@ function ($matches) use (&$parameters): string { */ public function getCsrfParamName(): string { - return $this->getApplication()->request->csrfParam; + return $this->getWebRequest()->csrfParam; } public function startApp(?\yii\log\Logger $logger = null): void @@ -268,7 +294,11 @@ public function startApp(?\yii\log\Logger $logger = null): void } $config = $this->mockMailer($config); - Yii::$app = Yii::createObject($config); + $app = Yii::createObject($config); + if (!$app instanceof \yii\base\Application) { + throw new ModuleConfigException($this, "Failed to initialize Yii2 app"); + } + \Yii::$app = $app; if ($logger instanceof \yii\log\Logger) { Yii::setLogger($logger); @@ -278,9 +308,9 @@ public function startApp(?\yii\log\Logger $logger = null): void } /** - * @param \Symfony\Component\BrowserKit\Request $request + * @param BrowserkitRequest $request */ - public function doRequest(object $request): \Symfony\Component\BrowserKit\Response + public function doRequest(object $request): Response { $_COOKIE = $request->getCookies(); $_SERVER = $request->getServer(); @@ -362,18 +392,13 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon $content = ob_get_clean(); if (empty($content) && !empty($response->content) && !isset($response->stream)) { throw new \Exception('No content was sent from Yii application'); + } elseif ($content === false) { + throw new \Exception('Failed to get output buffer'); } return new Response($content, $response->statusCode, $response->getHeaders()->toArray()); } - protected function revertErrorHandler() - { - $handler = new ErrorHandler(); - set_error_handler([$handler, 'errorHandler']); - } - - /** * Encodes the cookies and adds them to the headers. * @throws \yii\base\InvalidConfigException @@ -433,11 +458,19 @@ protected function mockMailer(array $config): array $mailerConfig = [ 'class' => TestMailer::class, - 'callback' => function (MessageInterface $message): void { + 'callback' => function (BaseMessage $message): void { $this->emails[] = $message; } ]; + if (isset($config['components'])) { + if (!is_array($config['components'])) { + throw new ModuleConfigException($this, + "Yii2 config does not contain components key is not of type array"); + } + } else { + $config['components'] = []; + } if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) { foreach ($config['components']['mailer'] as $name => $value) { if (in_array($name, $allowedOptions, true)) { @@ -511,8 +544,8 @@ protected function resetResponse(Application $app): void Debug::debug(<<|JsonSerializable $message + * @return void + */ protected function debug(string|array|JsonSerializable $message): void { $title = (new ReflectionClass($this))->getShortName(); if (is_array($message) || is_object($message)) { - $message = stripslashes(json_encode($message)); + $message = stripslashes(json_encode($message, JSON_THROW_ON_ERROR)); } codecept_debug("[$title] $message"); } diff --git a/src/Codeception/Lib/Connector/Yii2/Logger.php b/src/Codeception/Lib/Connector/Yii2/Logger.php index 820e4ba..cae874f 100644 --- a/src/Codeception/Lib/Connector/Yii2/Logger.php +++ b/src/Codeception/Lib/Connector/Yii2/Logger.php @@ -11,9 +11,16 @@ class Logger extends YiiLogger { + /** + * @var \SplQueue + */ private \SplQueue $logQueue; - public function __construct(private int $maxLogItems = 5, array $config = []) + /** + * @param int $maxLogItems + * @param array $config + */ + public function __construct(private readonly int $maxLogItems = 5, array $config = []) { parent::__construct($config); $this->logQueue = new \SplQueue(); @@ -25,7 +32,7 @@ public function init(): void } /** - * @param string|array|YiiException $message + * @param string|array|YiiException $message * @param self::LEVEL_INFO|self::LEVEL_WARNING|self::LEVEL_ERROR $level * @param string $category */ diff --git a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php index 350411a..ae9f19f 100644 --- a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php +++ b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php @@ -15,8 +15,17 @@ */ class TransactionForcer extends ConnectionWatcher { + /** + * @var array + */ private array $pdoCache = []; + /** + * @var array + */ private array $dsnCache = []; + /** + * @var array + */ private array $transactions = []; public function __construct(private bool $ignoreCollidingDSN) @@ -40,13 +49,13 @@ protected function connectionOpened(Connection $connection): void 'attributes' => $connection->attributes, 'emulatePrepare' => $connection->emulatePrepare, 'charset' => $connection->charset, - ])); + ], JSON_THROW_ON_ERROR)); /* * If keys match we assume connections are "similar enough". */ if (isset($this->pdoCache[$key])) { $connection->pdo = $this->pdoCache[$key]; - } else { + } elseif(isset($connection->pdo)) { $this->pdoCache[$key] = $connection->pdo; } if (isset($this->dsnCache[$connection->dsn]) diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index fdd47b8..57e9e7d 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -18,11 +18,16 @@ use Codeception\TestInterface; use ReflectionClass; use RuntimeException; -use Symfony\Component\BrowserKit\AbstractBrowser; +use Symfony\Component\BrowserKit\CookieJar; +use Symfony\Component\BrowserKit\History; use Yii; use yii\base\Security; use yii\db\ActiveQueryInterface; +use yii\db\ActiveRecordInterface; use yii\helpers\Url; +use yii\mail\BaseMessage; +use yii\mail\MessageInterface; +use yii\test\Fixture; use yii\web\Application; use yii\web\IdentityInterface; @@ -170,17 +175,65 @@ * Maintainer: **samdark** * Stability: **stable** * + * @phpstan-type ModuleConfig array{ + * fixturesMethod: string, + * cleanup: bool, + * ignoreCollidingDSN: bool, + * transaction: bool|null, + * entryScript: string, + * entryUrl: string, + * configFile: string|null, + * responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * recreateComponents: list, + * recreateApplication: bool, + * closeSessionOnRecreateApplication: bool, + * applicationClass: class-string<\yii\base\Application>|null + * } + * + * @phpstan-type ValidConfig array{ + * fixturesMethod: string, + * cleanup: bool, + * ignoreCollidingDSN: bool, + * transaction: bool|null, + * entryScript: string, + * entryUrl: string, + * configFile: string, + * responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * recreateComponents: list, + * recreateApplication: bool, + * closeSessionOnRecreateApplication: bool, + * applicationClass: class-string<\yii\base\Application>|null + * } + * @phpstan-type SessionBackup array{cookie: array, session: array, headers: array, clientContext: array{ cookieJar: CookieJar, history: History }} + * @phpstan-type ClientConfig array{ + * configFile: string, + * responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE, + * recreateComponents: list, + * recreateApplication: bool, + * closeSessionOnRecreateApplication: bool, + * applicationClass: class-string<\yii\base\Application>|null + * } */ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule { + /** + * @var list + */ + public array $loadedFixtures = []; + /** * Application config file must be set. + * @var ModuleConfig */ protected array $config = [ 'fixturesMethod' => '_fixtures', 'cleanup' => true, 'ignoreCollidingDSN' => false, 'transaction' => true, + 'configFile' => null, 'entryScript' => '', 'entryUrl' => 'http://localhost/index-test.php', 'responseCleanMethod' => Yii2Connector::CLEAN_CLEAR, @@ -191,13 +244,6 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule 'applicationClass' => null, ]; - protected array $requiredFields = ['configFile']; - - /** - * @var Yii2Connector\FixturesStore[] - */ - public array $loadedFixtures = []; - /** * Helper to manage database connections */ @@ -209,7 +255,7 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule private TransactionForcer $transactionForcer; /** - * @var array The contents of $_SERVER upon initialization of this object. + * @var array The contents of $_SERVER upon initialization of this object. * This is only used to restore it upon object destruction. * It MUST not be used anywhere else. */ @@ -247,6 +293,7 @@ protected function onReconfigure(): void { parent::onReconfigure(); $this->getClient()->resetApplication(); + $this->validateConfig(); $this->configureClient($this->config); $this->yiiLogger->getAndClearLog(); $this->getClient()->startApp($this->yiiLogger); @@ -272,9 +319,18 @@ private function initServerGlobal(): void ]); } + /** + * @phpstan-assert ValidConfig $this->config + */ protected function validateConfig(): void { parent::validateConfig(); + if (!isset($this->config['configFile'])) { + throw new ModuleConfigException( + self::class, + "The application config file was not configured" + ); + } $pathToConfig = codecept_absolute_path($this->config['configFile']); if (!is_file($pathToConfig)) { throw new ModuleConfigException( @@ -297,15 +353,20 @@ protected function validateConfig(): void } } - protected function configureClient(array $settings): void + + /** + * @param ClientConfig $settings + */ + private function configureClient(array $settings): void { - $settings['configFile'] = codecept_absolute_path($settings['configFile']); - foreach ($settings as $key => $value) { - if (property_exists($this->client, $key)) { - $this->getClient()->$key = $value; - } - } - $this->getClient()->resetApplication(); + $client = $this->getClient(); + $client->configFile = codecept_absolute_path($settings['configFile']); + $client->responseCleanMethod = $settings['responseCleanMethod']; + $client->requestCleanMethod = $settings['requestCleanMethod']; + $client->recreateApplication = $settings['recreateApplication']; + $client->closeSessionOnRecreateApplication = $settings['closeSessionOnRecreateApplication']; + $client->applicationClass = $settings['applicationClass']; + $client->resetApplication(); } /** @@ -324,6 +385,7 @@ protected function recreateClient(): void 'SERVER_PORT' => $parsedUrl['port'] ?? '80', 'HTTPS' => isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https' ]); + $this->validateConfig(); $this->configureClient($this->config); } @@ -355,11 +417,11 @@ private function loadFixtures(object $test): void { $this->debugSection('Fixtures', 'Loading fixtures'); if ($this->loadedFixtures === [] - && method_exists($test, $this->_getConfig('fixturesMethod')) + && method_exists($test, $this->config['fixturesMethod']) ) { $connectionWatcher = new ConnectionWatcher(); $connectionWatcher->start(); - $this->haveFixtures(call_user_func([$test, $this->_getConfig('fixturesMethod')])); + $this->haveFixtures($test->{$this->config['fixturesMethod']}); $connectionWatcher->stop(); $connectionWatcher->closeAll(); } @@ -395,6 +457,9 @@ public function _after(TestInterface $test): void parent::_after($test); } + /** + * @param \Exception $fail + */ public function _failed(TestInterface $test, $fail): void { $log = $this->yiiLogger->getAndClearLog(); @@ -422,6 +487,9 @@ protected function rollbackTransactions(): void } } + /** + * @return list + */ public function _parts(): array { return ['orm', 'init', 'fixtures', 'email', 'route']; @@ -486,10 +554,10 @@ public function amLoggedInAs(int|string|IdentityInterface $user): void * ``` * instead of calling `haveFixtures` in Cest `_before` * - * @param $fixtures * @part fixtures + * @param array $fixtures */ - public function haveFixtures($fixtures): void + public function haveFixtures(array $fixtures): void { if (empty($fixtures)) { return; @@ -505,17 +573,17 @@ public function haveFixtures($fixtures): void * Array of fixture instances * * @part fixtures + * @return array */ - public function grabFixtures() + public function grabFixtures(): array { - if ($this->loadedFixtures === []) { - return []; + $result = []; + foreach($this->loadedFixtures as $store) { + foreach($store->getFixtures() as $name => $fixture) { + $result[$name] = $fixture; + } } - - return array_merge(...array_map( - fn($fixturesStore) => $fixturesStore->getFixtures(), - $this->loadedFixtures - )); + return $result; } /** @@ -534,12 +602,10 @@ public function grabFixtures() * $user = $I->grabFixture('users', 'user1'); * ``` * - * @param $name - * @return mixed * @throws \Codeception\Exception\ModuleException if the fixture is not found * @part fixtures */ - public function grabFixture($name, $index = null) + public function grabFixture(string $name, null|string $index = null): Fixture|\yii\db\ActiveRecord|null { $fixtures = $this->grabFixtures(); if (!isset($fixtures[$name])) { @@ -627,9 +693,10 @@ public function dontSeeRecord(string $model, array $attributes = []): void * * @param class-string<\yii\db\ActiveRecord> $model * @param array $attributes + * @return ActiveRecordInterface|null|array * @part orm */ - public function grabRecord(string $model, array $attributes = []): \yii\db\ActiveRecord|null|array + public function grabRecord(string $model, array $attributes = []): ActiveRecordInterface|null|array { return $this->findRecord($model, $attributes); } @@ -637,8 +704,9 @@ public function grabRecord(string $model, array $attributes = []): \yii\db\Activ /** * @param class-string<\yii\db\ActiveRecord> $model Class name * @param array $attributes + * @return ActiveRecordInterface|null|array */ - protected function findRecord(string $model, array $attributes = []): \yii\db\ActiveRecord|null|array + protected function findRecord(string $model, array $attributes = []): ActiveRecordInterface|null|array { if (!class_exists($model)) { throw new RuntimeException("Class $model does not exist"); @@ -668,12 +736,12 @@ protected function findRecord(string $model, array $attributes = []): \yii\db\Ac * ``` * * @param string $route A route - * @param array $params Additional route parameters + * @param array $params Additional route parameters * @part route */ public function amOnRoute(string $route, array $params = []): void { - if (Yii::$app->controller === null) { + if (Yii::$app?->controller === null) { $route = "/{$route}"; } @@ -749,6 +817,7 @@ public function dontSeeEmailIsSent(): void * ``` * * @part email + * @return list List of sent emails * @throws \Codeception\Exception\ModuleException */ public function grabSentEmails(): array @@ -771,15 +840,17 @@ public function grabSentEmails(): array * ``` * @part email */ - public function grabLastSentEmail(): object + public function grabLastSentEmail(): BaseMessage|null { $this->seeEmailIsSent(); $messages = $this->grabSentEmails(); - return end($messages); + + return $messages[array_key_last($messages)] ?? null; } /** * Returns a list of regex patterns for recognized domain names + * @return non-empty-list */ public function getInternalDomains(): array { @@ -797,9 +868,9 @@ private function defineConstants(): void * Sets a cookie and, if validation is enabled, signs it. * @param string $name The name of the cookie * @param string $val The value of the cookie - * @param array $params Additional cookie params like `domain`, `path`, `expires` and `secure`. + * @param array{domain?: string, path?: string, expires?: int, secure?:bool} $params Additional cookie params like `domain`, `path`, `expires` and `secure`. */ - public function setCookie($name, $val, $params = []) + public function setCookie($name, $val, $params = []): void { parent::setCookie($name, $this->getClient()->hashCookieData($name, $val), $params); } @@ -838,7 +909,7 @@ public function _initializeSession(): void /** * Return the session content for future restoring. Implements MultiSession. - * @return array backup data + * @return SessionBackup */ public function _backupSession(): array { @@ -848,6 +919,7 @@ public function _backupSession(): array return [ 'clientContext' => $this->getClient()->getContext(), 'headers' => $this->headers, + // @phpstan-ignore nullCoalesce.variable 'cookie' => $_COOKIE ?? [], 'session' => $_SESSION ?? [], ]; @@ -855,7 +927,7 @@ public function _backupSession(): array /** * Restore a session. Implements MultiSession. - * @param array $session output of _backupSession() + * @param SessionBackup $session output of _backupSession() */ public function _loadSession($session): void { @@ -872,6 +944,7 @@ public function _loadSession($session): void /** * Close and dump a session. Implements MultiSession. + * @param mixed $session */ public function _closeSession($session = null): void { From af9bd31bf3bf8486f95470e7b94d777732bd513a Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 11:50:05 +0100 Subject: [PATCH 3/8] chore(ci): dont run on php < 8.3 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2492a31..7e7c9e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - php: [8.0, 8.1, 8.2, 8.3, 8.4] + php: [8.3, 8.4] steps: - name: Checkout code From 5ff88682d0a9e2c0d8044bca04b85009ceb1c81e Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 11:51:49 +0100 Subject: [PATCH 4/8] fix: call fixtures method --- src/Codeception/Module/Yii2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index 57e9e7d..e3c3749 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -421,7 +421,7 @@ private function loadFixtures(object $test): void ) { $connectionWatcher = new ConnectionWatcher(); $connectionWatcher->start(); - $this->haveFixtures($test->{$this->config['fixturesMethod']}); + $this->haveFixtures($test->{$this->config['fixturesMethod']}()); $connectionWatcher->stop(); $connectionWatcher->closeAll(); } From 09da620bc653b0849cf0f47c24b51dab403be969 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 15:07:05 +0100 Subject: [PATCH 5/8] chore: clean up code for better readability --- src/Codeception/Lib/Connector/Yii2.php | 33 ++++++++++++-------------- src/Codeception/Module/Yii2.php | 24 ++----------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index d9ff2cd..3716578 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -15,6 +15,7 @@ use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\Request as BrowserkitRequest; +use yii\web\Request as YiiRequest; use Symfony\Component\BrowserKit\Response; use Yii; use yii\base\Component; @@ -123,11 +124,11 @@ protected function getApplication(): \yii\base\Application return Yii::$app ?? throw new \RuntimeException('Failed to create Yii2 application'); } - private function getWebRequest(): Request + private function getWebRequest(): YiiRequest { $request = $this->getApplication()->request; - if (!$request instanceof Request) { - throw new \RuntimeException('Request component is not of type ' . Request::class); + if (!$request instanceof YiiRequest) { + throw new \RuntimeException('Request component is not of type ' . YiiRequest::class); } return $request; } @@ -181,15 +182,11 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void */ public function hashCookieData(string $name, string $value): string { - $app = $this->getApplication(); - $request = $app->getRequest(); - if (!$request instanceof Request) { - throw new \RuntimeException("Can't do cookie operations on non-web requests"); - } + $request = $this->getWebRequest(); if (!$request->enableCookieValidation) { return $value; } - return $app->security->hashData(serialize([$name, $value]), $request->cookieValidationKey); + return $this->getApplication()->security->hashData(serialize([$name, $value]), $request->cookieValidationKey); } /** @@ -367,9 +364,9 @@ public function doRequest(object $request): Response * Sending the response is problematic because it tries to send headers. */ $app->trigger($app::EVENT_BEFORE_REQUEST); - $response = $app->handleRequest($yiiRequest); + $yiiResponse = $app->handleRequest($yiiRequest); $app->trigger($app::EVENT_AFTER_REQUEST); - $response->send(); + $yiiResponse->send(); } catch (\Exception $e) { if ($e instanceof UserException) { // Don't discard output and pass exception handling to Yii to be able @@ -383,20 +380,20 @@ public function doRequest(object $request): Response $response = $app->response; } - $this->encodeCookies($response, $yiiRequest, $app->security); + $this->encodeCookies($yiiResponse, $yiiRequest, $app->security); - if ($response->isRedirection) { - Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true)); + if ($yiiResponse->isRedirection) { + Debug::debug("[Redirect with headers]" . print_r($yiiResponse->getHeaders()->toArray(), true)); } $content = ob_get_clean(); - if (empty($content) && !empty($response->content) && !isset($response->stream)) { + if (empty($content) && !empty($yiiResponse->content) && !isset($yiiResponse->stream)) { throw new \Exception('No content was sent from Yii application'); } elseif ($content === false) { throw new \Exception('Failed to get output buffer'); } - return new Response($content, $response->statusCode, $response->getHeaders()->toArray()); + return new Response($content, $yiiResponse->statusCode, $yiiResponse->getHeaders()->toArray()); } /** @@ -405,7 +402,7 @@ public function doRequest(object $request): Response */ protected function encodeCookies( YiiResponse $response, - Request $request, + YiiRequest $request, Security $security ): void { if ($request->enableCookieValidation) { @@ -520,7 +517,7 @@ public function setContext(array $context): void */ public function closeSession(): void { - $app = \Yii::$app; + $app = $this->getApplication(); if ($app instanceof \yii\web\Application && $app->has('session', true)) { $app->session->close(); } diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index e3c3749..cfa9eee 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -22,6 +22,7 @@ use Symfony\Component\BrowserKit\History; use Yii; use yii\base\Security; +use yii\web\Application as WebApplication; use yii\db\ActiveQueryInterface; use yii\db\ActiveRecordInterface; use yii\helpers\Url; @@ -749,27 +750,6 @@ public function amOnRoute(string $route, array $params = []): void $this->amOnPage(Url::to($params)); } - /** - * Gets a component from the Yii container. Throws an exception if the - * component is not available - * - * ```php - * grabComponent('mailer'); - * ``` - * - * @throws \Codeception\Exception\ModuleException - * @deprecated in your tests you can use \Yii::$app directly. - */ - public function grabComponent(string $component): null|object - { - try { - return $this->getClient()->getComponent($component); - } catch (ConfigurationException $e) { - throw new ModuleException($this, $e->getMessage()); - } - } - /** * Checks that an email is sent. * @@ -913,7 +893,7 @@ public function _initializeSession(): void */ public function _backupSession(): array { - if (Yii::$app instanceof Application && Yii::$app->has('session', true) && Yii::$app->session->useCustomStorage) { + if (Yii::$app instanceof WebApplication && Yii::$app->has('session', true) && Yii::$app->session->useCustomStorage) { throw new ModuleException($this, "Yii2 MultiSession only supports the default session backend."); } return [ From 4df941fb8e49179eff1c5cec0d011843c71e5177 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 15:09:21 +0100 Subject: [PATCH 6/8] fix: use runtimeexception when applicable --- src/Codeception/Lib/Connector/Yii2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index 3716578..9b6eb1f 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -388,9 +388,9 @@ public function doRequest(object $request): Response $content = ob_get_clean(); if (empty($content) && !empty($yiiResponse->content) && !isset($yiiResponse->stream)) { - throw new \Exception('No content was sent from Yii application'); + throw new \RuntimeException('No content was sent from Yii application'); } elseif ($content === false) { - throw new \Exception('Failed to get output buffer'); + throw new \RuntimeException('Failed to get output buffer'); } return new Response($content, $yiiResponse->statusCode, $yiiResponse->getHeaders()->toArray()); From 619fb2d13074183a8d7b52b443348e075825292d Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 16:14:39 +0100 Subject: [PATCH 7/8] fix: variable rename --- src/Codeception/Lib/Connector/Yii2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index 9b6eb1f..d6f89cf 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -377,7 +377,7 @@ public function doRequest(object $request): Response // for exceptions not related to Http, we pass them to Codeception throw $e; } - $response = $app->response; + $yiiResponse = $app->response; } $this->encodeCookies($yiiResponse, $yiiRequest, $app->security); From 0e2ae2a08fc2fb5259d526707c6da2a6bfbe71fd Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 16:23:13 +0100 Subject: [PATCH 8/8] chore: remove grabComponent calls since it was removed after deprecation --- tests/cases/sqlite/functional/SqLiteCest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cases/sqlite/functional/SqLiteCest.php b/tests/cases/sqlite/functional/SqLiteCest.php index 1670ee0..ea3b4c7 100644 --- a/tests/cases/sqlite/functional/SqLiteCest.php +++ b/tests/cases/sqlite/functional/SqLiteCest.php @@ -25,15 +25,15 @@ public function _fixtures() public function testSharedPDO(FunctionalTester $I) { /** @var Connection $db1 */ - $db1 = $I->grabComponent('db1'); + $db1 = \Yii::$app->get('db1'); $I->assertSame(['test'], $db1->schema->getTableNames('', true)); /** @var Connection $db21 */ - $db21 = $I->grabComponent('db21'); + $db21 = \Yii::$app->get('db21'); $I->assertSame(['test'], $db21->schema->getTableNames('', true)); /** @var Connection $db22 */ - $db22 = $I->grabComponent('db22'); + $db22 = \Yii::$app->get('db22'); $I->assertSame(['test'], $db22->schema->getTableNames('', true)); } @@ -41,7 +41,7 @@ public function testSharedPDO(FunctionalTester $I) public function testTransaction(FunctionalTester $I) { /** @var Connection $db1 */ - $db1 = $I->grabComponent('db1'); + $db1 = \Yii::$app->get('db1'); $I->assertFalse($db1->isActive); $db1->open(); $I->assertNotNull($db1->getTransaction());