From 785bdfa6a140a501a5a4b7ffdca96b4845d7ee19 Mon Sep 17 00:00:00 2001 From: TavoNiievez Date: Thu, 20 Feb 2025 23:34:58 -0500 Subject: [PATCH 1/4] Implement new PHP features --- phpstan.neon | 5 +- src/Codeception/Lib/Connector/Yii2.php | 80 +++++------ .../Lib/Connector/Yii2/ConnectionWatcher.php | 9 +- .../Lib/Connector/Yii2/FixturesStore.php | 9 +- src/Codeception/Lib/Connector/Yii2/Logger.php | 35 ++--- .../Lib/Connector/Yii2/TestMailer.php | 16 +-- .../Lib/Connector/Yii2/TransactionForcer.php | 25 +--- src/Codeception/Module/Yii2.php | 131 ++++++++---------- 8 files changed, 126 insertions(+), 184 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index efda0f0..7019dd5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,12 +9,11 @@ parameters: paths: - src checkMaybeUndefinedVariables: true - checkGenericClassInNonGenericObjectType: false ignoreErrors: # All Yii setters accept `null` but their phpdoc is incorrect. - message: '~^Parameter #1 \$(.+) of method yii\\web\\Request::set(.+)\(\) expects (.+), null given.$~' path: 'src/' - - message: '~^Variable \$_COOKIE in isset\(\) always exists and is not nullable.$~' - path: 'src/' + # If you want to ignore missing generics errors in the future, you can add: + # - identifier: missingType.generics stubFiles: - tests/Yii.stub diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index 890ab45..8a41272 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -114,7 +114,7 @@ public function getApplication(): \yii\base\Application public function resetApplication(bool $closeSession = true): void { codecept_debug('Destroying application'); - if (true === $closeSession) { + if ($closeSession) { $this->closeSession(); } Yii::$app = null; @@ -141,13 +141,13 @@ public function findAndLoginUser(int|string|IdentityInterface $user): void throw new ConfigurationException('The user component is not configured'); } - if ($user instanceof \yii\web\IdentityInterface) { + if ($user instanceof IdentityInterface) { $identity = $user; } else { // class name implementing IdentityInterface $identityClass = $userComponent->identityClass; - $identity = call_user_func([$identityClass, 'findIdentity'], $user); - if (!isset($identity)) { + $identity = $identityClass::findIdentity($user); + if ($identity === null) { throw new \RuntimeException('User not found'); } } @@ -233,7 +233,7 @@ private function getDomainRegex(string $template): string '/<(?:\w+):?([^>]+)?>/u', function ($matches) use (&$parameters) { $key = '__' . count($parameters) . '__'; - $parameters[$key] = isset($matches[1]) ? $matches[1] : '\w+'; + $parameters[$key] = $matches[1] ?? '\w+'; return $key; }, $template @@ -258,15 +258,10 @@ public function startApp(?\yii\log\Logger $logger = null): void codecept_debug('Starting application'); $config = require($this->configFile); if (!isset($config['class'])) { - if (null !== $this->applicationClass) { - $config['class'] = $this->applicationClass; - } else { - $config['class'] = 'yii\web\Application'; - } + $config['class'] = $this->applicationClass ?? 'yii\web\Application'; } - if (isset($config['container'])) - { + if (isset($config['container'])) { Yii::configure(Yii::$container, $config['container']); unset($config['container']); } @@ -326,9 +321,6 @@ public function doRequest(object $request): \Symfony\Component\BrowserKit\Respon $target->enabled = false; } - - - $yiiRequest = $app->getRequest(); if ($request->getContent() !== null) { $yiiRequest->setRawBody($request->getContent()); @@ -441,7 +433,7 @@ protected function mockMailer(array $config): array $mailerConfig = [ 'class' => TestMailer::class, - 'callback' => function (MessageInterface $message) { + 'callback' => function (MessageInterface $message): void { $this->emails[] = $message; } ]; @@ -474,7 +466,7 @@ public function getContext(): array { return [ 'cookieJar' => $this->cookieJar, - 'history' => $this->history, + 'history' => $this->history, ]; } @@ -509,11 +501,12 @@ protected function resetResponse(Application $app): void { $method = $this->responseCleanMethod; // First check the current response object. - if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND) + if ( + ($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND) || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE) - || count($app->response->getBehaviors()) > 0 - ) && $method === self::CLEAN_RECREATE + || count($app->response->getBehaviors()) > 0) + && $method === self::CLEAN_RECREATE ) { Debug::debug(<<set('response', $app->getComponents()['response']); - break; - case self::CLEAN_CLEAR: - $app->response->clear(); - break; - case self::CLEAN_MANUAL: - break; - } + match ($method) { + self::CLEAN_FORCE_RECREATE, self::CLEAN_RECREATE => $app->set('response', $app->getComponents()['response']), + self::CLEAN_CLEAR => $app->response->clear(), + self::CLEAN_MANUAL => null, + default => throw new \InvalidArgumentException("Unknown method: $method"), + }; } protected function resetRequest(Application $app): void @@ -555,28 +543,24 @@ protected function resetRequest(Application $app): void $method = self::CLEAN_CLEAR; } - switch ($method) { - case self::CLEAN_FORCE_RECREATE: - case self::CLEAN_RECREATE: - $app->set('request', $app->getComponents()['request']); - break; - case self::CLEAN_CLEAR: + match ($method) { + self::CLEAN_FORCE_RECREATE, self::CLEAN_RECREATE => $app->set('request', $app->getComponents()['request']), + self::CLEAN_CLEAR => (function () use ($request): void { $request->getHeaders()->removeAll(); $request->setBaseUrl(null); $request->setHostInfo(null); - $request->setPathInfo(null); - $request->setScriptFile(null); - $request->setScriptUrl(null); - $request->setUrl(null); + $request->setPathInfo(''); + $request->setScriptFile(''); + $request->setScriptUrl(''); + $request->setUrl(''); $request->setPort(0); $request->setSecurePort(0); - $request->setAcceptableContentTypes(null); - $request->setAcceptableLanguages(null); - - break; - case self::CLEAN_MANUAL: - break; - } + $request->setAcceptableContentTypes([]); + $request->setAcceptableLanguages([]); + })(), + self::CLEAN_MANUAL => null, + default => throw new \InvalidArgumentException("Unknown method: $method"), + }; } /** diff --git a/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php b/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php index aa92f7f..323801b 100644 --- a/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php +++ b/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php @@ -6,6 +6,7 @@ use yii\base\Event; use yii\db\Connection; +use ReflectionClass; /** * Class ConnectionWatcher @@ -21,7 +22,7 @@ class ConnectionWatcher public function __construct() { - $this->handler = function (Event $event) { + $this->handler = function (Event $event): void { if ($event->sender instanceof Connection) { $this->connectionOpened($event->sender); } @@ -55,10 +56,10 @@ public function closeAll(): void } } - protected function debug($message): void + protected function debug(mixed $message): void { - $title = (new \ReflectionClass($this))->getShortName(); - if (is_array($message) or is_object($message)) { + $title = (new ReflectionClass($this))->getShortName(); + if (is_array($message) || is_object($message)) { $message = stripslashes(json_encode($message)); } codecept_debug("[$title] $message"); diff --git a/src/Codeception/Lib/Connector/Yii2/FixturesStore.php b/src/Codeception/Lib/Connector/Yii2/FixturesStore.php index 71c490e..a1fce57 100644 --- a/src/Codeception/Lib/Connector/Yii2/FixturesStore.php +++ b/src/Codeception/Lib/Connector/Yii2/FixturesStore.php @@ -11,25 +11,24 @@ class FixturesStore { use FixtureTrait; - protected $data; + protected mixed $data; /** * Expects fixtures config * * FixturesStore constructor. - * @param $data */ - public function __construct($data) + public function __construct(mixed $data) { $this->data = $data; } - public function fixtures() + public function fixtures(): mixed { return $this->data; } - public function globalFixtures() + public function globalFixtures(): array { return [ 'initDbFixture' => [ diff --git a/src/Codeception/Lib/Connector/Yii2/Logger.php b/src/Codeception/Lib/Connector/Yii2/Logger.php index 0a2501f..3a7d62e 100644 --- a/src/Codeception/Lib/Connector/Yii2/Logger.php +++ b/src/Codeception/Lib/Connector/Yii2/Logger.php @@ -6,12 +6,14 @@ use Codeception\Util\Debug; use yii\helpers\VarDumper; +use yii\base\Exception as YiiException; +use yii\log\Logger as YiiLogger; -class Logger extends \yii\log\Logger +class Logger extends YiiLogger { private \SplQueue $logQueue; - public function __construct(private int $maxLogItems = 5, $config = []) + public function __construct(private int $maxLogItems = 5, array $config = []) { parent::__construct($config); $this->logQueue = new \SplQueue(); @@ -23,33 +25,28 @@ public function init(): void } /** - * @param string|array|\yii\base\Exception $message - * @param $level - * @param $category - * @return void + * @param string|array|YiiException $message + * @param mixed $level + * @param mixed $category */ public function log($message, $level, $category = 'application'): void { if (!in_array($level, [ - \yii\log\Logger::LEVEL_INFO, - \yii\log\Logger::LEVEL_WARNING, - \yii\log\Logger::LEVEL_ERROR, - ])) { + self::LEVEL_INFO, + self::LEVEL_WARNING, + self::LEVEL_ERROR, + ], true)) { return; } if (str_starts_with($category, 'yii\db\Command')) { return; // don't log queries } - // https://github.com/Codeception/Codeception/issues/3696 - if ($message instanceof \yii\base\Exception) { + if ($message instanceof YiiException) { $message = $message->__toString(); } - $logMessage = "[$category] " . VarDumper::export($message); - Debug::debug($logMessage); - $this->logQueue->enqueue($logMessage); if ($this->logQueue->count() > $this->maxLogItems) { $this->logQueue->dequeue(); @@ -58,12 +55,8 @@ public function log($message, $level, $category = 'application'): void public function getAndClearLog(): string { - $completeStr = ''; - foreach ($this->logQueue as $item) { - $completeStr .= $item . PHP_EOL; - } + $logs = iterator_to_array($this->logQueue); $this->logQueue = new \SplQueue(); - - return $completeStr; + return implode(PHP_EOL, $logs) . PHP_EOL; } } diff --git a/src/Codeception/Lib/Connector/Yii2/TestMailer.php b/src/Codeception/Lib/Connector/Yii2/TestMailer.php index c6276a4..230e15a 100644 --- a/src/Codeception/Lib/Connector/Yii2/TestMailer.php +++ b/src/Codeception/Lib/Connector/Yii2/TestMailer.php @@ -4,26 +4,24 @@ namespace Codeception\Lib\Connector\Yii2; +use Closure; use yii\mail\BaseMailer; class TestMailer extends BaseMailer { public $messageClass = \yii\symfonymailer\Message::class; - /** - * @var \Closure - */ - public $callback; + public Closure $callback; - protected function sendMessage($message) + protected function sendMessage(mixed $message): bool { - call_user_func($this->callback, $message); + ($this->callback)($message); return true; } - - protected function saveMessage($message) + + protected function saveMessage(mixed $message): bool { - call_user_func($this->callback, $message); + ($this->callback)($message); return true; } } diff --git a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php index 7c2588a..1e02965 100644 --- a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php +++ b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php @@ -5,7 +5,6 @@ namespace Codeception\Lib\Connector\Yii2; use Codeception\Util\Debug; -use yii\base\Event; use yii\db\Connection; use yii\db\Transaction; @@ -16,22 +15,17 @@ */ class TransactionForcer extends ConnectionWatcher { - private $ignoreCollidingDSN; + private bool $ignoreCollidingDSN; + private array $pdoCache = []; + private array $dsnCache = []; + private array $transactions = []; - private $pdoCache = []; - - private $dsnCache; - - private $transactions = []; - - public function __construct( - $ignoreCollidingDSN - ) { + public function __construct(bool $ignoreCollidingDSN) + { parent::__construct(); $this->ignoreCollidingDSN = $ignoreCollidingDSN; } - protected function connectionOpened(Connection $connection): void { parent::connectionOpened($connection); @@ -47,9 +41,8 @@ protected function connectionOpened(Connection $connection): void 'pass' => $connection->password, 'attributes' => $connection->attributes, 'emulatePrepare' => $connection->emulatePrepare, - 'charset' => $connection->charset + 'charset' => $connection->charset, ])); - /* * If keys match we assume connections are "similar enough". */ @@ -58,7 +51,6 @@ protected function connectionOpened(Connection $connection): void } else { $this->pdoCache[$key] = $connection->pdo; } - if (isset($this->dsnCache[$connection->dsn]) && $this->dsnCache[$connection->dsn] !== $key && !$this->ignoreCollidingDSN @@ -72,12 +64,10 @@ protected function connectionOpened(Connection $connection): void ); Debug::pause(); } - if (isset($this->transactions[$key])) { $this->debug('Reusing PDO, so no need for a new transaction'); return; } - $this->debug('Transaction started for: ' . $connection->dsn); $this->transactions[$key] = $connection->beginTransaction(); } @@ -91,7 +81,6 @@ public function rollbackAll(): void $this->debug('Transaction cancelled; all changes reverted.'); } } - $this->transactions = []; $this->pdoCache = []; $this->dsnCache = []; diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index 99a3a2d..5d4f062 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -16,7 +16,8 @@ use Codeception\Lib\Interfaces\MultiSession; use Codeception\Lib\Interfaces\PartedModule; use Codeception\TestInterface; -use Symfony\Component\DomCrawler\Crawler as SymfonyCrawler; +use ReflectionClass; +use RuntimeException; use Yii; use yii\base\Security; use yii\db\ActiveQueryInterface; @@ -44,7 +45,7 @@ * interaction. * * You may use multiple database connections, each will use a separate * transaction; to prevent accidental mistakes we will warn you if you try to - * connect to the same database twice but we cannot reuse the same connection. + * connect to the same database twice, but we cannot reuse the same connection. * * ## Config * @@ -97,7 +98,7 @@ * * ## Parts * - * By default all available methods are loaded, but you can also use the `part` + * By default, all available methods are loaded, but you can also use the `part` * option to select only the needed actions and to avoid conflicts. The * available parts are: * @@ -123,7 +124,7 @@ * configFile: 'config/test.php' * part: [orm, route] # allow to use AR methods and route method * transaction: false # don't wrap test in transaction - * cleanup: false # don't cleanup the fixtures + * cleanup: false # don't clean up the fixtures * entryScript: index-test.php * ``` * @@ -216,12 +217,12 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule private Logger $yiiLogger; - private function getClient(): \Codeception\Lib\Connector\Yii2|null + private function getClient(): ?\Codeception\Lib\Connector\Yii2 { - if (isset($this->client) && !$this->client instanceof \Codeception\Lib\Connector\Yii2) { - throw new \RuntimeException('The Yii2 module must be used with the Yii2 browser client'); + if (isset($this->client) && !($this->client instanceof \Codeception\Lib\Connector\Yii2)) { + throw new RuntimeException('The Yii2 module must be used with the Yii2 browser client'); } - return $this->client; + return $this->client ?? null; } public function _initialize(): void @@ -235,7 +236,6 @@ public function _initialize(): void $this->initServerGlobal(); } - /** * Module configuration changed inside a test. * We always re-create the application. @@ -256,23 +256,22 @@ protected function onReconfigure(): void */ private function initServerGlobal(): void { - $entryUrl = $this->config['entryUrl']; + $parsedUrl = parse_url($entryUrl); $entryFile = $this->config['entryScript'] ?: basename($entryUrl); - $entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH); + $entryScript = $this->config['entryScript'] ?: ($parsedUrl['path'] ?? ''); $_SERVER = array_merge($_SERVER, [ 'SCRIPT_FILENAME' => $entryFile, 'SCRIPT_NAME' => $entryScript, - 'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST), - 'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80', - 'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https' + 'SERVER_NAME' => $parsedUrl['host'] ?? '', + 'SERVER_PORT' => $parsedUrl['port'] ?? '80', + 'HTTPS' => isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https' ]); } protected function validateConfig(): void { parent::validateConfig(); - $pathToConfig = codecept_absolute_path($this->config['configFile']); if (!is_file($pathToConfig)) { throw new ModuleConfigException( @@ -280,18 +279,17 @@ protected function validateConfig(): void "The application config file does not exist: " . $pathToConfig ); } - - if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS)) { + $validMethods = implode(", ", Yii2Connector::CLEAN_METHODS); + if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) { throw new ModuleConfigException( __CLASS__, - "The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS) + "The response clean method must be one of: " . $validMethods ); } - - if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS)) { + if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) { throw new ModuleConfigException( __CLASS__, - "The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS) + "The request clean method must be one of: " . $validMethods ); } } @@ -299,7 +297,6 @@ protected function validateConfig(): void protected 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; @@ -314,17 +311,16 @@ protected function configureClient(array $settings): void protected function recreateClient(): void { $entryUrl = $this->config['entryUrl']; + $parsedUrl = parse_url($entryUrl); $entryFile = $this->config['entryScript'] ?: basename($entryUrl); - $entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_PATH); - + $entryScript = $this->config['entryScript'] ?: ($parsedUrl['path'] ?? ''); $this->client = new Yii2Connector([ 'SCRIPT_FILENAME' => $entryFile, 'SCRIPT_NAME' => $entryScript, - 'SERVER_NAME' => parse_url($entryUrl, PHP_URL_HOST), - 'SERVER_PORT' => parse_url($entryUrl, PHP_URL_PORT) ?: '80', - 'HTTPS' => parse_url($entryUrl, PHP_URL_SCHEME) === 'https' + 'SERVER_NAME' => $parsedUrl['host'] ?? '', + 'SERVER_PORT' => $parsedUrl['port'] ?? '80', + 'HTTPS' => isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'https' ]); - $this->configureClient($this->config); } @@ -346,7 +342,6 @@ public function _before(TestInterface $test): void $this->loadFixtures($test); } - $this->startTransactions(); } @@ -386,7 +381,7 @@ public function _after(TestInterface $test): void $this->loadedFixtures = []; } - $this->getClient()?->resetApplication(); + $this->getClient()->resetApplication(); if (isset($this->connectionWatcher)) { $this->connectionWatcher->stop(); @@ -426,7 +421,7 @@ protected function rollbackTransactions(): void public function _parts(): array { - return ['orm', 'init', 'fixtures', 'email','route']; + return ['orm', 'init', 'fixtures', 'email', 'route']; } /** @@ -449,10 +444,8 @@ public function _parts(): array public function amLoggedInAs(int|string|IdentityInterface $user): void { try { - $this->getClient()?->findAndLoginUser($user); - } catch (ConfigurationException $e) { - throw new ModuleException($this, $e->getMessage()); - } catch (\RuntimeException $e) { + $this->getClient()->findAndLoginUser($user); + } catch (ConfigurationException|RuntimeException $e) { throw new ModuleException($this, $e->getMessage()); } } @@ -517,15 +510,10 @@ public function grabFixtures() return []; } - return call_user_func_array( - 'array_merge', - array_map( // merge all fixtures from all fixture stores - function ($fixturesStore) { - return $fixturesStore->getFixtures(); - }, - $this->loadedFixtures - ) - ); + return array_merge(...array_map( + fn($fixturesStore) => $fixturesStore->getFixtures(), + $this->loadedFixtures + )); } /** @@ -556,13 +544,11 @@ public function grabFixture($name, $index = null) throw new ModuleException($this, "Fixture $name is not loaded"); } $fixture = $fixtures[$name]; - if ($index === null) { - return $fixture; - } - if ($fixture instanceof \yii\test\BaseActiveFixture) { - return $fixture->getModel($index); - } - throw new ModuleException($this, "Fixture $name is not an instance of ActiveFixture and can't be loaded with second parameter"); + return match (true) { + $index === null => $fixture, + $fixture instanceof \yii\test\BaseActiveFixture => $fixture->getModel($index), + default => throw new ModuleException($this, "Fixture $name is not an instance of ActiveFixture and can't be loaded with second parameter") + }; } /** @@ -581,7 +567,7 @@ public function grabFixture($name, $index = null) */ public function haveRecord(string $model, $attributes = []): mixed { - /** @var T $record **/ + /** @var T $record */ $record = \Yii::createObject($model); $record->setAttributes($attributes, false); $res = $record->save(false); @@ -594,7 +580,7 @@ public function haveRecord(string $model, $attributes = []): mixed /** * Checks that a record exists in the database. * - * ``` php + * ```php * $I->seeRecord('app\models\User', array('name' => 'davert')); * ``` * @@ -614,7 +600,7 @@ public function seeRecord(string $model, array $attributes = []): void /** * Checks that a record does not exist in the database. * - * ``` php + * ```php * $I->dontSeeRecord('app\models\User', array('name' => 'davert')); * ``` * @@ -634,7 +620,7 @@ public function dontSeeRecord(string $model, array $attributes = []): void /** * Retrieves a record from the database * - * ``` php + * ```php * $category = $I->grabRecord('app\models\User', array('name' => 'davert')); * ``` * @@ -651,12 +637,12 @@ public function grabRecord(string $model, array $attributes = []): \yii\db\Activ * @param class-string<\yii\db\ActiveRecord> $model Class name * @param array $attributes */ - protected function findRecord(string $model, array $attributes = []): \yii\db\ActiveRecord | null | array + protected function findRecord(string $model, array $attributes = []): \yii\db\ActiveRecord|null|array { if (!class_exists($model)) { - throw new \RuntimeException("Class $model does not exist"); + throw new RuntimeException("Class $model does not exist"); } - $rc = new \ReflectionClass($model); + $rc = new ReflectionClass($model); if ($rc->hasMethod('find') /** @phpstan-ignore-next-line */ && ($findMethod = $rc->getMethod('find')) @@ -668,10 +654,9 @@ protected function findRecord(string $model, array $attributes = []): \yii\db\Ac if ($activeQuery instanceof ActiveQueryInterface) { return $activeQuery->andWhere($attributes)->one(); } - - throw new \RuntimeException("$model::find() must return an instance of yii\db\QueryInterface"); + throw new RuntimeException("$model::find() must return an instance of yii\db\QueryInterface"); } - throw new \RuntimeException("Class $model does not have a public static find() method without required parameters"); + throw new RuntimeException("Class $model does not have a public static find() method without required parameters"); } /** @@ -683,14 +668,14 @@ protected function findRecord(string $model, array $attributes = []): \yii\db\Ac * * @param string $route A route * @param array $params Additional route parameters - * @part route + * @part route */ public function amOnRoute(string $route, array $params = []): void { if (Yii::$app->controller === null) { $route = "/{$route}"; } - + array_unshift($params, $route); $this->amOnPage(Url::to($params)); } @@ -794,8 +779,6 @@ public function grabLastSentEmail(): object return end($messages); } - - /** * Returns a list of regex patterns for recognized domain names * @@ -868,14 +851,14 @@ public function _backupSession(): array return [ 'clientContext' => $this->getClient()->getContext(), 'headers' => $this->headers, - 'cookie' => isset($_COOKIE) ? $_COOKIE : [], - 'session' => isset($_SESSION) ? $_SESSION : [], + 'cookie' => $_COOKIE, + 'session' => $_SESSION, ]; } /** * Restore a session. Implements MultiSession. - * @param array $session output of _backupSession() + * @param array $session output of _backupSession() */ public function _loadSession($session): void { @@ -883,14 +866,10 @@ public function _loadSession($session): void $this->headers = $session['headers']; $_SESSION = $session['session']; $_COOKIE = $session['cookie']; - - // reset Yii::$app->user - if (isset(Yii::$app)) { - $app = Yii::$app; - $definitions = $app->getComponents(true); - if ($app->has('user', true)) { - $app->set('user', $definitions['user']); - } + $app = Yii::$app; + if ($app?->has('user', true)) { + $definitions = $app->getComponents(); + $app->set('user', $definitions['user']); } } From 66cc4fc5d76964301c827e64169af0f86ba53bfa Mon Sep 17 00:00:00 2001 From: TavoNiievez Date: Fri, 21 Feb 2025 09:22:40 -0500 Subject: [PATCH 2/4] Fix PR comments --- composer.json | 3 +- src/Codeception/Lib/Connector/Yii2.php | 29 +++++++++---------- .../Lib/Connector/Yii2/ConnectionWatcher.php | 8 +++-- .../Lib/Connector/Yii2/FixturesStore.php | 8 ++--- src/Codeception/Lib/Connector/Yii2/Logger.php | 4 +-- .../Lib/Connector/Yii2/TransactionForcer.php | 4 +-- src/Codeception/Module/Yii2.php | 28 +++++++----------- 7 files changed, 39 insertions(+), 45 deletions(-) diff --git a/composer.json b/composer.json index a447850..8cb1f78 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "codemix/yii2-localeurls": "^1.7", "codeception/module-asserts": ">= 3.0", "codeception/module-filesystem": "> 3.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10", + "rector/rector": "^1.2" }, "autoload":{ "classmap": ["src/"] diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index 8a41272..7708fba 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -181,7 +181,7 @@ public function getInternalDomains(): array if ($urlManager->enablePrettyUrl) { foreach ($urlManager->rules as $rule) { /** @var \yii\web\UrlRule $rule */ - if (isset($rule->host)) { + if ($rule->host !== null) { $domains[] = $this->getDomainRegex($rule->host); } } @@ -228,10 +228,10 @@ private function getDomainRegex(string $template): string $template = $matches[1]; } $parameters = []; - if (strpos($template, '<') !== false) { + if (str_contains($template, '<')) { $template = preg_replace_callback( '/<(?:\w+):?([^>]+)?>/u', - function ($matches) use (&$parameters) { + function ($matches) use (&$parameters): string { $key = '__' . count($parameters) . '__'; $parameters[$key] = $matches[1] ?? '\w+'; return $key; @@ -258,7 +258,7 @@ public function startApp(?\yii\log\Logger $logger = null): void codecept_debug('Starting application'); $config = require($this->configFile); if (!isset($config['class'])) { - $config['class'] = $this->applicationClass ?? 'yii\web\Application'; + $config['class'] = $this->applicationClass ?? \yii\web\Application::class; } if (isset($config['container'])) { @@ -267,10 +267,9 @@ public function startApp(?\yii\log\Logger $logger = null): void } $config = $this->mockMailer($config); - /** @var \yii\base\Application $app */ Yii::$app = Yii::createObject($config); - if ($logger !== null) { + if ($logger instanceof \yii\log\Logger) { Yii::setLogger($logger); } else { Yii::setLogger(new Logger()); @@ -502,9 +501,9 @@ protected function resetResponse(Application $app): void $method = $this->responseCleanMethod; // First check the current response object. if ( - ($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND) - || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND) - || $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE) + ($app->response->hasEventHandlers(YiiResponse::EVENT_BEFORE_SEND) + || $app->response->hasEventHandlers(YiiResponse::EVENT_AFTER_SEND) + || $app->response->hasEventHandlers(YiiResponse::EVENT_AFTER_PREPARE) || count($app->response->getBehaviors()) > 0) && $method === self::CLEAN_RECREATE ) { @@ -549,14 +548,14 @@ protected function resetRequest(Application $app): void $request->getHeaders()->removeAll(); $request->setBaseUrl(null); $request->setHostInfo(null); - $request->setPathInfo(''); - $request->setScriptFile(''); - $request->setScriptUrl(''); - $request->setUrl(''); + $request->setPathInfo(null); + $request->setScriptFile(null); + $request->setScriptUrl(null); + $request->setUrl(null); $request->setPort(0); $request->setSecurePort(0); - $request->setAcceptableContentTypes([]); - $request->setAcceptableLanguages([]); + $request->setAcceptableContentTypes(null); + $request->setAcceptableLanguages(null); })(), self::CLEAN_MANUAL => null, default => throw new \InvalidArgumentException("Unknown method: $method"), diff --git a/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php b/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php index 323801b..1240662 100644 --- a/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php +++ b/src/Codeception/Lib/Connector/Yii2/ConnectionWatcher.php @@ -4,9 +4,11 @@ namespace Codeception\Lib\Connector\Yii2; +use Closure; +use JsonSerializable; +use ReflectionClass; use yii\base\Event; use yii\db\Connection; -use ReflectionClass; /** * Class ConnectionWatcher @@ -15,7 +17,7 @@ */ class ConnectionWatcher { - private \Closure $handler; + private Closure $handler; /** @var Connection[] */ private array $connections = []; @@ -56,7 +58,7 @@ public function closeAll(): void } } - protected function debug(mixed $message): void + protected function debug(string|array|JsonSerializable $message): void { $title = (new ReflectionClass($this))->getShortName(); if (is_array($message) || is_object($message)) { diff --git a/src/Codeception/Lib/Connector/Yii2/FixturesStore.php b/src/Codeception/Lib/Connector/Yii2/FixturesStore.php index a1fce57..cb46a77 100644 --- a/src/Codeception/Lib/Connector/Yii2/FixturesStore.php +++ b/src/Codeception/Lib/Connector/Yii2/FixturesStore.php @@ -11,16 +11,13 @@ class FixturesStore { use FixtureTrait; - protected mixed $data; - /** * Expects fixtures config * * FixturesStore constructor. */ - public function __construct(mixed $data) + public function __construct(protected mixed $data) { - $this->data = $data; } public function fixtures(): mixed @@ -28,6 +25,9 @@ public function fixtures(): mixed return $this->data; } + /** + * @return array{initDbFixture: array{class: class-string}} + */ public function globalFixtures(): array { return [ diff --git a/src/Codeception/Lib/Connector/Yii2/Logger.php b/src/Codeception/Lib/Connector/Yii2/Logger.php index 3a7d62e..aa96f66 100644 --- a/src/Codeception/Lib/Connector/Yii2/Logger.php +++ b/src/Codeception/Lib/Connector/Yii2/Logger.php @@ -5,8 +5,8 @@ namespace Codeception\Lib\Connector\Yii2; use Codeception\Util\Debug; -use yii\helpers\VarDumper; use yii\base\Exception as YiiException; +use yii\helpers\VarDumper; use yii\log\Logger as YiiLogger; class Logger extends YiiLogger @@ -26,7 +26,7 @@ public function init(): void /** * @param string|array|YiiException $message - * @param mixed $level + * @param self::LEVEL_INFO|self::LEVEL_WARNING|self::LEVEL_ERROR $level * @param mixed $category */ public function log($message, $level, $category = 'application'): void diff --git a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php index 1e02965..350411a 100644 --- a/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php +++ b/src/Codeception/Lib/Connector/Yii2/TransactionForcer.php @@ -15,15 +15,13 @@ */ class TransactionForcer extends ConnectionWatcher { - private bool $ignoreCollidingDSN; private array $pdoCache = []; private array $dsnCache = []; private array $transactions = []; - public function __construct(bool $ignoreCollidingDSN) + public function __construct(private bool $ignoreCollidingDSN) { parent::__construct(); - $this->ignoreCollidingDSN = $ignoreCollidingDSN; } protected function connectionOpened(Connection $connection): void diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index 5d4f062..d4781ea 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -18,6 +18,7 @@ use Codeception\TestInterface; use ReflectionClass; use RuntimeException; +use Symfony\Component\BrowserKit\AbstractBrowser; use Yii; use yii\base\Security; use yii\db\ActiveQueryInterface; @@ -174,7 +175,6 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule { /** * Application config file must be set. - * @var array */ protected array $config = [ 'fixturesMethod' => '_fixtures', @@ -217,9 +217,9 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule private Logger $yiiLogger; - private function getClient(): ?\Codeception\Lib\Connector\Yii2 + private function getClient(): ?Yii2Connector { - if (isset($this->client) && !($this->client instanceof \Codeception\Lib\Connector\Yii2)) { + if ($this->client instanceof AbstractBrowser && !($this->client instanceof Yii2Connector)) { throw new RuntimeException('The Yii2 module must be used with the Yii2 browser client'); } return $this->client ?? null; @@ -275,20 +275,20 @@ protected function validateConfig(): void $pathToConfig = codecept_absolute_path($this->config['configFile']); if (!is_file($pathToConfig)) { throw new ModuleConfigException( - __CLASS__, + self::class, "The application config file does not exist: " . $pathToConfig ); } $validMethods = implode(", ", Yii2Connector::CLEAN_METHODS); if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) { throw new ModuleConfigException( - __CLASS__, + self::class, "The response clean method must be one of: " . $validMethods ); } if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) { throw new ModuleConfigException( - __CLASS__, + self::class, "The request clean method must be one of: " . $validMethods ); } @@ -351,7 +351,7 @@ public function _before(TestInterface $test): void private function loadFixtures(object $test): void { $this->debugSection('Fixtures', 'Loading fixtures'); - if (empty($this->loadedFixtures) + if ($this->loadedFixtures === [] && method_exists($test, $this->_getConfig('fixturesMethod')) ) { $connectionWatcher = new ConnectionWatcher(); @@ -502,11 +502,10 @@ public function haveFixtures($fixtures): void * Array of fixture instances * * @part fixtures - * @return array */ public function grabFixtures() { - if (!$this->loadedFixtures) { + if ($this->loadedFixtures === []) { return []; } @@ -562,7 +561,6 @@ public function grabFixture($name, $index = null) * @template T of \yii\db\ActiveRecord * @param class-string $model * @param array $attributes - * @return mixed * @part orm */ public function haveRecord(string $model, $attributes = []): mixed @@ -713,7 +711,6 @@ public function grabComponent(string $component): null|object * $I->seeEmailIsSent(3); * ``` * - * @param int|null $num * @throws \Codeception\Exception\ModuleException * @part email */ @@ -749,7 +746,6 @@ public function dontSeeEmailIsSent(): void * ``` * * @part email - * @return array * @throws \Codeception\Exception\ModuleException */ public function grabSentEmails(): array @@ -781,8 +777,6 @@ public function grabLastSentEmail(): object /** * Returns a list of regex patterns for recognized domain names - * - * @return array */ public function getInternalDomains(): array { @@ -791,9 +785,9 @@ public function getInternalDomains(): array private function defineConstants(): void { - defined('YII_DEBUG') or define('YII_DEBUG', true); - defined('YII_ENV') or define('YII_ENV', 'test'); - defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', false); + defined('YII_DEBUG') || define('YII_DEBUG', true); + defined('YII_ENV') || define('YII_ENV', 'test'); + defined('YII_ENABLE_ERROR_HANDLER') || define('YII_ENABLE_ERROR_HANDLER', false); } /** From dc4ce156b56e39bcc425d0a21d0def8f959cf28e Mon Sep 17 00:00:00 2001 From: TavoNiievez Date: Fri, 21 Feb 2025 11:14:16 -0500 Subject: [PATCH 3/4] Fix PR comments --- src/Codeception/Lib/Connector/Yii2.php | 5 +++-- src/Codeception/Lib/Connector/Yii2/Logger.php | 2 +- src/Codeception/Module/Yii2.php | 11 +++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Codeception/Lib/Connector/Yii2.php b/src/Codeception/Lib/Connector/Yii2.php index 7708fba..b7df1d5 100644 --- a/src/Codeception/Lib/Connector/Yii2.php +++ b/src/Codeception/Lib/Connector/Yii2.php @@ -8,6 +8,7 @@ use Codeception\Lib\Connector\Yii2\Logger; use Codeception\Lib\Connector\Yii2\TestMailer; use Codeception\Util\Debug; +use InvalidArgumentException; use Symfony\Component\BrowserKit\AbstractBrowser as Client; use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\CookieJar; @@ -521,7 +522,7 @@ protected function resetResponse(Application $app): void self::CLEAN_FORCE_RECREATE, self::CLEAN_RECREATE => $app->set('response', $app->getComponents()['response']), self::CLEAN_CLEAR => $app->response->clear(), self::CLEAN_MANUAL => null, - default => throw new \InvalidArgumentException("Unknown method: $method"), + default => throw new InvalidArgumentException("Unknown method: $method"), }; } @@ -558,7 +559,7 @@ protected function resetRequest(Application $app): void $request->setAcceptableLanguages(null); })(), self::CLEAN_MANUAL => null, - default => throw new \InvalidArgumentException("Unknown method: $method"), + default => throw new InvalidArgumentException("Unknown method: $method"), }; } diff --git a/src/Codeception/Lib/Connector/Yii2/Logger.php b/src/Codeception/Lib/Connector/Yii2/Logger.php index aa96f66..820e4ba 100644 --- a/src/Codeception/Lib/Connector/Yii2/Logger.php +++ b/src/Codeception/Lib/Connector/Yii2/Logger.php @@ -27,7 +27,7 @@ public function init(): void /** * @param string|array|YiiException $message * @param self::LEVEL_INFO|self::LEVEL_WARNING|self::LEVEL_ERROR $level - * @param mixed $category + * @param string $category */ public function log($message, $level, $category = 'application'): void { diff --git a/src/Codeception/Module/Yii2.php b/src/Codeception/Module/Yii2.php index d4781ea..ac8de14 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -217,12 +217,15 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule private Logger $yiiLogger; - private function getClient(): ?Yii2Connector + private function getClient(): Yii2Connector { - if ($this->client instanceof AbstractBrowser && !($this->client instanceof Yii2Connector)) { + if (!isset($this->client)) { + throw new RuntimeException('Browser not initialized'); + } + if (!$this->client instanceof Yii2Connector) { throw new RuntimeException('The Yii2 module must be used with the Yii2 browser client'); } - return $this->client ?? null; + return $this->client; } public function _initialize(): void @@ -846,7 +849,7 @@ public function _backupSession(): array 'clientContext' => $this->getClient()->getContext(), 'headers' => $this->headers, 'cookie' => $_COOKIE, - 'session' => $_SESSION, + 'session' => $_SESSION ?? [], ]; } From 93c9135f708d6ad16adcd6739da66045e0c23631 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Mon, 24 Feb 2025 09:41:38 +0100 Subject: [PATCH 4/4] chore: --- 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 ac8de14..fdd47b8 100644 --- a/src/Codeception/Module/Yii2.php +++ b/src/Codeception/Module/Yii2.php @@ -848,7 +848,7 @@ public function _backupSession(): array return [ 'clientContext' => $this->getClient()->getContext(), 'headers' => $this->headers, - 'cookie' => $_COOKIE, + 'cookie' => $_COOKIE ?? [], 'session' => $_SESSION ?? [], ]; }