first commit

This commit is contained in:
2026-01-25 18:18:09 +08:00
commit 509312e604
8136 changed files with 2349298 additions and 0 deletions

21
vendor/codeception/module-yii2/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011 Michael Bodnarchuk and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,55 @@
{
"name":"codeception/module-yii2",
"description":"Codeception module for Yii2 framework",
"keywords":["codeception", "yii2"],
"homepage":"https://codeception.com/",
"type":"library",
"license":"MIT",
"authors":[
{
"name":"Alexander Makarov"
},
{
"name":"Sam Mouse"
},
{
"name":"Michael Bodnarchuk"
}
],
"minimum-stability": "RC",
"require": {
"php": "^8.0",
"codeception/codeception": "^5.0.8",
"codeception/lib-innerbrowser": "^3.0 | ^4.0"
},
"require-dev": {
"yiisoft/yii2": "dev-master",
"yiisoft/yii2-app-advanced": "dev-master",
"codeception/verify": "^3.0",
"codemix/yii2-localeurls": "^1.7",
"codeception/module-asserts": ">= 3.0",
"codeception/module-filesystem": "> 3.0",
"phpstan/phpstan": "^1.10"
},
"autoload":{
"classmap": ["src/"]
},
"autoload-dev": {
"classmap": [
"vendor/yiisoft/yii2/Yii.php",
"tests/cases"
]
},
"config": {
"allow-plugins": {
"yiisoft/yii2-composer": true
},
"classmap-authoritative": true
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
]
}

View File

@ -0,0 +1,20 @@
#includes:
# - phpstan-baseline.neon
parameters:
reportUnmatchedIgnoredErrors: true
dynamicConstantNames:
- CONSOLE
- YII_DEBUG
level: 5
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/'
stubFiles:
- tests/Yii.stub

View File

@ -0,0 +1,604 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Exception\ConfigurationException;
use Codeception\Lib\Connector\Yii2\Logger;
use Codeception\Lib\Connector\Yii2\TestMailer;
use Codeception\Util\Debug;
use Symfony\Component\BrowserKit\AbstractBrowser as Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\CookieJar;
use Symfony\Component\BrowserKit\History;
use Symfony\Component\BrowserKit\Response;
use Yii;
use yii\base\ExitException;
use yii\base\Security;
use yii\base\UserException;
use yii\mail\MessageInterface;
use yii\web\Application;
use yii\web\ErrorHandler;
use yii\web\IdentityInterface;
use yii\web\Request;
use yii\web\Response as YiiResponse;
use yii\web\User;
class Yii2 extends Client
{
use Shared\PhpSuperGlobalsConverter;
const CLEAN_METHODS = [
self::CLEAN_RECREATE,
self::CLEAN_CLEAR,
self::CLEAN_FORCE_RECREATE,
self::CLEAN_MANUAL
];
/**
* Clean the response object by recreating it.
* This might lose behaviors / event handlers / other changes that are done in the application bootstrap phase.
*/
const CLEAN_RECREATE = 'recreate';
/**
* Same as recreate but will not warn when behaviors / event handlers are lost.
*/
const CLEAN_FORCE_RECREATE = 'force_recreate';
/**
* Clean the response object by resetting specific properties via its' `clear()` method.
* This will keep behaviors / event handlers, but could inadvertently leave some changes intact.
* @see \yii\web\Response::clear()
*/
const CLEAN_CLEAR = 'clear';
/**
* Do not clean the response, instead the test writer will be responsible for manually resetting the response in
* between requests during one test
*/
const CLEAN_MANUAL = 'manual';
/**
* @var string application config file
*/
public $configFile;
/**
* @var string method for cleaning the response object before each request
*/
public $responseCleanMethod;
/**
* @var string method for cleaning the request object before each request
*/
public $requestCleanMethod;
/**
* @var string[] List of component names that must be recreated before each request
*/
public $recreateComponents = [];
/**
* This option is there primarily for backwards compatibility.
* It means you cannot make any modification to application state inside your app, since they will get discarded.
* @var bool whether to recreate the whole application before each request
*/
public $recreateApplication = false;
/**
* @var bool whether to close the session in between requests inside a single test, if recreateApplication is set to true
*/
public bool $closeSessionOnRecreateApplication = true;
/**
* @var class-string The FQN of the application class to use. In a default Yii setup, should be either `yii\web\Application`
* or `yii\console\Application`
*/
public string|null $applicationClass = null;
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
{
if (!isset(Yii::$app)) {
$this->startApp();
}
return Yii::$app;
}
public function resetApplication(bool $closeSession = true): void
{
codecept_debug('Destroying application');
if (true === $closeSession) {
$this->closeSession();
}
Yii::$app = null;
\yii\web\UploadedFile::reset();
if (method_exists(\yii\base\Event::class, 'offAll')) {
\yii\base\Event::offAll();
}
Yii::setLogger(null);
// This resolves an issue with database connections not closing properly.
gc_collect_cycles();
}
/**
* Finds and logs in a user
* @internal
* @throws ConfigurationException
* @throws \RuntimeException
*/
public function findAndLoginUser(int|string|IdentityInterface $user): void
{
$app = $this->getApplication();
$userComponent = $app->get('user');
if (!$userComponent instanceof User) {
throw new ConfigurationException('The user component is not configured');
}
if ($user instanceof \yii\web\IdentityInterface) {
$identity = $user;
} else {
// class name implementing IdentityInterface
$identityClass = $userComponent->identityClass;
$identity = call_user_func([$identityClass, 'findIdentity'], $user);
if (!isset($identity)) {
throw new \RuntimeException('User not found');
}
}
$userComponent->login($identity);
}
/**
* @internal
* @param string $name The name of the cookie
* @param string $value The value of the cookie
* @return string The value to send to the browser
*/
public function hashCookieData($name, $value): string
{
$app = $this->getApplication();
if (!$app->request->enableCookieValidation) {
return $value;
}
return $app->security->hashData(serialize([$name, $value]), $app->request->cookieValidationKey);
}
/**
* @internal
* @return array 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) {
/** @var \yii\web\UrlRule $rule */
if (isset($rule->host)) {
$domains[] = $this->getDomainRegex($rule->host);
}
}
}
return array_unique($domains);
}
/**
* @internal
* @return array List of sent emails
*/
public function getEmails(): array
{
return $this->emails;
}
/**
* Deletes all stored emails.
* @internal
*/
public function clearEmails(): void
{
$this->emails = [];
}
/**
* @internal
*/
public function getComponent($name)
{
$app = $this->getApplication();
if (!$app->has($name)) {
throw new ConfigurationException("Component $name is not available in current application");
}
return $app->get($name);
}
/**
* Getting domain regex from rule host template
*/
private function getDomainRegex(string $template): string
{
if (preg_match('#https?://(.*)#', $template, $matches)) {
$template = $matches[1];
}
$parameters = [];
if (strpos($template, '<') !== false) {
$template = preg_replace_callback(
'/<(?:\w+):?([^>]+)?>/u',
function ($matches) use (&$parameters) {
$key = '__' . count($parameters) . '__';
$parameters[$key] = isset($matches[1]) ? $matches[1] : '\w+';
return $key;
},
$template
);
}
$template = preg_quote($template);
$template = strtr($template, $parameters);
return '/^' . $template . '$/u';
}
/**
* Gets the name of the CSRF param.
* @internal
*/
public function getCsrfParamName(): string
{
return $this->getApplication()->request->csrfParam;
}
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';
}
}
if (isset($config['container']))
{
Yii::configure(Yii::$container, $config['container']);
unset($config['container']);
}
$config = $this->mockMailer($config);
/** @var \yii\base\Application $app */
Yii::$app = Yii::createObject($config);
if ($logger !== null) {
Yii::setLogger($logger);
} else {
Yii::setLogger(new Logger());
}
}
/**
* @param \Symfony\Component\BrowserKit\Request $request
*/
public function doRequest(object $request): \Symfony\Component\BrowserKit\Response
{
$_COOKIE = $request->getCookies();
$_SERVER = $request->getServer();
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = $_GET = [];
if (strtoupper($request->getMethod()) === 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
$uri = $request->getUri();
$pathString = parse_url($uri, PHP_URL_PATH);
$queryString = parse_url($uri, PHP_URL_QUERY);
$_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString;
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['QUERY_STRING'] = (string)$queryString;
parse_str($queryString ?: '', $params);
foreach ($params as $k => $v) {
$_GET[$k] = $v;
}
ob_start();
$this->beforeRequest();
$app = $this->getApplication();
if (!$app instanceof Application) {
throw new ConfigurationException("Application is not a web application");
}
// disabling logging. Logs are slowing test execution down
foreach ($app->log->targets as $target) {
$target->enabled = false;
}
$yiiRequest = $app->getRequest();
if ($request->getContent() !== null) {
$yiiRequest->setRawBody($request->getContent());
$yiiRequest->setBodyParams(null);
} else {
$yiiRequest->setRawBody(null);
$yiiRequest->setBodyParams($_POST);
}
$yiiRequest->setQueryParams($_GET);
try {
/*
* This is basically equivalent to $app->run() without sending the response.
* Sending the response is problematic because it tries to send headers.
*/
$app->trigger($app::EVENT_BEFORE_REQUEST);
$response = $app->handleRequest($yiiRequest);
$app->trigger($app::EVENT_AFTER_REQUEST);
$response->send();
} catch (\Exception $e) {
if ($e instanceof UserException) {
// Don't discard output and pass exception handling to Yii to be able
// to expect error response codes in tests.
$app->errorHandler->discardExistingOutput = false;
$app->errorHandler->handleException($e);
} elseif (!$e instanceof ExitException) {
// for exceptions not related to Http, we pass them to Codeception
throw $e;
}
$response = $app->response;
}
$this->encodeCookies($response, $yiiRequest, $app->security);
if ($response->isRedirection) {
Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true));
}
$content = ob_get_clean();
if (empty($content) && !empty($response->content) && !isset($response->stream)) {
throw new \Exception('No content was sent from Yii application');
}
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
*/
protected function encodeCookies(
YiiResponse $response,
Request $request,
Security $security
): void {
if ($request->enableCookieValidation) {
$validationKey = $request->cookieValidationKey;
}
foreach ($response->getCookies() as $cookie) {
/** @var \yii\web\Cookie $cookie */
$value = $cookie->value;
// Expire = 1 means we're removing the cookie
if ($cookie->expire !== 1 && isset($validationKey)) {
$data = version_compare(Yii::getVersion(), '2.0.2', '>')
? [$cookie->name, $cookie->value]
: $cookie->value;
$value = $security->hashData(serialize($data), $validationKey);
}
$c = new Cookie(
$cookie->name,
$value,
$cookie->expire,
$cookie->path,
$cookie->domain,
$cookie->secure,
$cookie->httpOnly
);
$this->getCookieJar()->set($c);
}
}
/**
* Replace mailer with in memory mailer
* @param array<string, mixed> $config Original configuration
* @return array<string, mixed> New configuration
*/
protected function mockMailer(array $config): array
{
// options that make sense for mailer mock
$allowedOptions = [
'htmlLayout',
'textLayout',
'messageConfig',
'messageClass',
'useFileTransport',
'fileTransportPath',
'fileTransportCallback',
'view',
'viewPath',
];
$mailerConfig = [
'class' => TestMailer::class,
'callback' => function (MessageInterface $message) {
$this->emails[] = $message;
}
];
if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) {
foreach ($config['components']['mailer'] as $name => $value) {
if (in_array($name, $allowedOptions, true)) {
$mailerConfig[$name] = $value;
}
}
}
$config['components']['mailer'] = $mailerConfig;
return $config;
}
public function restart(): void
{
parent::restart();
$this->resetApplication();
}
/**
* Return an assoc array with the client context: cookieJar, history.
*
* @internal
* @return array{ cookieJar: CookieJar, history: History }
*/
public function getContext(): array
{
return [
'cookieJar' => $this->cookieJar,
'history' => $this->history,
];
}
/**
* Set the context, see getContext().
*
* @param array{ cookieJar: CookieJar, history: History } $context
*/
public function setContext(array $context): void
{
$this->cookieJar = $context['cookieJar'];
$this->history = $context['history'];
}
/**
* This functions closes the session of the application, if the application exists and has a session.
* @internal
*/
public function closeSession(): void
{
$app = \Yii::$app;
if ($app instanceof \yii\web\Application && $app->has('session', true)) {
$app->session->close();
}
}
/**
* Resets the applications' response object.
* The method used depends on the module configuration.
*/
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)
|| count($app->response->getBehaviors()) > 0
) && $method === self::CLEAN_RECREATE
) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
the response object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the response. If you are certain you want to recreate it, please configure
responseCleanMethod = 'force_recreate' in the module.
TEXT
);
$method = self::CLEAN_CLEAR;
}
switch ($method) {
case self::CLEAN_FORCE_RECREATE:
case self::CLEAN_RECREATE:
$app->set('response', $app->getComponents()['response']);
break;
case self::CLEAN_CLEAR:
$app->response->clear();
break;
case self::CLEAN_MANUAL:
break;
}
}
protected function resetRequest(Application $app): void
{
$method = $this->requestCleanMethod;
$request = $app->request;
// First check the current request object.
if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the request object. But the Yii2 module is configured to recreate
the request object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the request. If you are certain you want to recreate it, please configure
requestCleanMethod = 'force_recreate' in the module.
TEXT
);
$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:
$request->getHeaders()->removeAll();
$request->setBaseUrl(null);
$request->setHostInfo(null);
$request->setPathInfo(null);
$request->setScriptFile(null);
$request->setScriptUrl(null);
$request->setUrl(null);
$request->setPort(0);
$request->setSecurePort(0);
$request->setAcceptableContentTypes(null);
$request->setAcceptableLanguages(null);
break;
case self::CLEAN_MANUAL:
break;
}
}
/**
* Called before each request, preparation happens here.
*/
protected function beforeRequest(): void
{
if ($this->recreateApplication) {
$this->resetApplication($this->closeSessionOnRecreateApplication);
return;
}
$application = $this->getApplication();
if (!$application instanceof Application) {
throw new ConfigurationException('Application must be an instance of web application when doing requests');
}
$this->resetResponse($application);
$this->resetRequest($application);
$definitions = $application->getComponents(true);
foreach ($this->recreateComponents as $component) {
// Only recreate if it has actually been instantiated.
if ($application->has($component, true)) {
$application->set($component, $definitions[$component]);
}
}
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\base\Event;
use yii\db\Connection;
/**
* Class ConnectionWatcher
* This class will watch for new database connection and store a reference to the connection object.
* @package Codeception\Lib\Connector\Yii2
*/
class ConnectionWatcher
{
private \Closure $handler;
/** @var Connection[] */
private array $connections = [];
public function __construct()
{
$this->handler = function (Event $event) {
if ($event->sender instanceof Connection) {
$this->connectionOpened($event->sender);
}
};
}
protected function connectionOpened(Connection $connection): void
{
$this->debug('Connection opened!');
$this->connections[] = $connection;
}
public function start(): void
{
Event::on(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('watching new connections');
}
public function stop(): void
{
Event::off(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('no longer watching new connections');
}
public function closeAll(): void
{
$count = count($this->connections);
$this->debug("closing all ($count) connections");
foreach ($this->connections as $connection) {
$connection->close();
}
}
protected function debug($message): void
{
$title = (new \ReflectionClass($this))->getShortName();
if (is_array($message) or is_object($message)) {
$message = stripslashes(json_encode($message));
}
codecept_debug("[$title] $message");
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
class FixturesStore
{
use FixtureTrait;
protected $data;
/**
* Expects fixtures config
*
* FixturesStore constructor.
* @param $data
*/
public function __construct($data)
{
$this->data = $data;
}
public function fixtures()
{
return $this->data;
}
public function globalFixtures()
{
return [
'initDbFixture' => [
'class' => InitDbFixture::class,
],
];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use Codeception\Util\Debug;
use yii\helpers\VarDumper;
class Logger extends \yii\log\Logger
{
private \SplQueue $logQueue;
public function __construct(private int $maxLogItems = 5, $config = [])
{
parent::__construct($config);
$this->logQueue = new \SplQueue();
}
public function init(): void
{
// overridden to prevent register_shutdown_function
}
/**
* @param string|array<mixed>|\yii\base\Exception $message
* @param $level
* @param $category
* @return void
*/
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,
])) {
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) {
$message = $message->__toString();
}
$logMessage = "[$category] " . VarDumper::export($message);
Debug::debug($logMessage);
$this->logQueue->enqueue($logMessage);
if ($this->logQueue->count() > $this->maxLogItems) {
$this->logQueue->dequeue();
}
}
public function getAndClearLog(): string
{
$completeStr = '';
foreach ($this->logQueue as $item) {
$completeStr .= $item . PHP_EOL;
}
$this->logQueue = new \SplQueue();
return $completeStr;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\mail\BaseMailer;
class TestMailer extends BaseMailer
{
public $messageClass = \yii\symfonymailer\Message::class;
/**
* @var \Closure
*/
public $callback;
protected function sendMessage($message)
{
call_user_func($this->callback, $message);
return true;
}
protected function saveMessage($message)
{
call_user_func($this->callback, $message);
return true;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use Codeception\Util\Debug;
use yii\base\Event;
use yii\db\Connection;
use yii\db\Transaction;
/**
* Class TransactionForcer
* This class adds support for forcing transactions as well as reusing PDO objects.
* @package Codeception\Lib\Connector\Yii2
*/
class TransactionForcer extends ConnectionWatcher
{
private $ignoreCollidingDSN;
private $pdoCache = [];
private $dsnCache;
private $transactions = [];
public function __construct(
$ignoreCollidingDSN
) {
parent::__construct();
$this->ignoreCollidingDSN = $ignoreCollidingDSN;
}
protected function connectionOpened(Connection $connection): void
{
parent::connectionOpened($connection);
/**
* We should check if the known PDO objects are the same, in which case we should reuse the PDO
* object so only 1 transaction is started and multiple connections to the same database see the
* same data (due to writes inside a transaction not being visible from the outside).
*
*/
$key = md5(json_encode([
'dsn' => $connection->dsn,
'user' => $connection->username,
'pass' => $connection->password,
'attributes' => $connection->attributes,
'emulatePrepare' => $connection->emulatePrepare,
'charset' => $connection->charset
]));
/*
* If keys match we assume connections are "similar enough".
*/
if (isset($this->pdoCache[$key])) {
$connection->pdo = $this->pdoCache[$key];
} else {
$this->pdoCache[$key] = $connection->pdo;
}
if (isset($this->dsnCache[$connection->dsn])
&& $this->dsnCache[$connection->dsn] !== $key
&& !$this->ignoreCollidingDSN
) {
$this->debug(<<<TEXT
You use multiple connections to the same DSN ({$connection->dsn}) with different configuration.
These connections will not see the same database state since we cannot share a transaction between different PDO
instances.
You can remove this message by adding 'ignoreCollidingDSN = true' in the module configuration.
TEXT
);
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();
}
public function rollbackAll(): void
{
/** @var Transaction $transaction */
foreach ($this->transactions as $transaction) {
if ($transaction->db->isActive) {
$transaction->rollBack();
$this->debug('Transaction cancelled; all changes reverted.');
}
}
$this->transactions = [];
$this->pdoCache = [];
$this->dsnCache = [];
}
}

View File

@ -0,0 +1,901 @@
<?php
namespace Codeception\Module;
use Codeception\Exception\ConfigurationException;
use Codeception\Exception\ModuleConfigException;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Connector\Yii2 as Yii2Connector;
use Codeception\Lib\Connector\Yii2\ConnectionWatcher;
use Codeception\Lib\Connector\Yii2\Logger;
use Codeception\Lib\Connector\Yii2\TransactionForcer;
use Codeception\Lib\Framework;
use Codeception\Lib\Interfaces\ActiveRecord;
use Codeception\Lib\Interfaces\MultiSession;
use Codeception\Lib\Interfaces\PartedModule;
use Codeception\TestInterface;
use Symfony\Component\DomCrawler\Crawler as SymfonyCrawler;
use Yii;
use yii\base\Security;
use yii\db\ActiveQueryInterface;
use yii\helpers\Url;
use yii\web\Application;
use yii\web\IdentityInterface;
/**
* This module provides integration with [Yii framework](https://www.yiiframework.com/) (2.0).
*
* It initializes the Yii framework in a test environment and provides actions
* for functional testing.
*
* ## Application state during testing
*
* This section details what you can expect when using this module.
*
* * You will get a fresh application in `\Yii::$app` at the start of each test
* (available in the test and in `_before()`).
* * Inside your test you may change application state; however these changes
* will be lost when doing a request if you have enabled `recreateApplication`.
* * When executing a request via one of the request functions the `request`
* and `response` component are both recreated.
* * After a request the whole application is available for inspection /
* 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.
*
* ## Config
*
* * `configFile` *required* - path to the application config file. The file
* should be configured for the test environment and return a configuration
* array.
* * `applicationClass` - Fully qualified class name for the application. There are
* several ways to define the application class. Either via a `class` key in the Yii
* config, via specifying this codeception module configuration value or let codeception
* use its default value `yii\web\Application`. In a standard Yii application, this
* value should be either `yii\console\Application`, `yii\web\Application` or unset.
* * `entryUrl` - initial application url (default: http://localhost/index-test.php).
* * `entryScript` - front script title (like: index-test.php). If not set it's
* taken from `entryUrl`.
* * `transaction` - (default: `true`) wrap all database connection inside a
* transaction and roll it back after the test. Should be disabled for
* acceptance testing.
* * `cleanup` - (default: `true`) cleanup fixtures after the test
* * `ignoreCollidingDSN` - (default: `false`) When 2 database connections use
* the same DSN but different settings an exception will be thrown. Set this to
* true to disable this behavior.
* * `fixturesMethod` - (default: `_fixtures`) Name of the method used for
* creating fixtures.
* * `responseCleanMethod` - (default: `clear`) Method for cleaning the
* response object. Note that this is only for multiple requests inside a
* single test case. Between test cases the whole application is always
* recreated.
* * `requestCleanMethod` - (default: `recreate`) Method for cleaning the
* request object. Note that this is only for multiple requests inside a single
* test case. Between test cases the whole application is always recreated.
* * `recreateComponents` - (default: `[]`) Some components change their state
* making them unsuitable for processing multiple requests. In production
* this is usually not a problem since web apps tend to die and start over
* after each request. This allows you to list application components that
* need to be recreated before each request. As a consequence, any
* components specified here should not be changed inside a test since those
* changes will get discarded.
* * `recreateApplication` - (default: `false`) whether to recreate the whole
* application before each request
*
* You can use this module by setting params in your `functional.suite.yml`:
*
* ```yaml
* actor: FunctionalTester
* modules:
* enabled:
* - Yii2:
* configFile: 'path/to/config.php'
* ```
*
* ## Parts
*
* 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:
*
* * `init` - use the module only for initialization (for acceptance tests).
* * `orm` - include only `haveRecord/grabRecord/seeRecord/dontSeeRecord` actions.
* * `fixtures` - use fixtures inside tests with `haveFixtures/grabFixture/grabFixtures` actions.
* * `email` - include email actions `seeEmailsIsSent/grabLastSentEmail/...`
*
* See [WebDriver module](https://codeception.com/docs/modules/WebDriver#Loading-Parts-from-other-Modules)
* for general information on how to load parts of a framework module.
*
* ### Example (`acceptance.suite.yml`)
*
* ```yaml
* actor: AcceptanceTester
* modules:
* enabled:
* - WebDriver:
* url: http://127.0.0.1:8080/
* browser: firefox
* - Yii2:
* configFile: 'config/test.php'
* part: orm # allow to use AR methods
* transaction: false # don't wrap test in transaction
* cleanup: false # don't cleanup the fixtures
* entryScript: index-test.php
* ```
*
* ## Fixtures
*
* This module allows to use
* [fixtures](https://www.yiiframework.com/doc-2.0/guide-test-fixtures.html)
* inside a test. There are two ways to do that. Fixtures can either be loaded
* with the [haveFixtures](#haveFixtures) method inside a test:
*
* ```php
* <?php
* $I->haveFixtures(['posts' => PostsFixture::class]);
* ```
*
* or, if you need to load fixtures before the test, you
* can specify fixtures in the `_fixtures` method of a test case:
*
* ```php
* <?php
* // inside Cest file or Codeception\TestCase\Unit
* public function _fixtures()
* {
* return ['posts' => PostsFixture::class]
* }
* ```
*
* ## URL
*
* With this module you can also use Yii2's URL format for all codeception
* commands that expect a URL:
*
* ```php
* <?php
* $I->amOnPage('index-test.php?r=site/index');
* $I->amOnPage('http://localhost/index-test.php?r=site/index');
* $I->sendAjaxPostRequest(['/user/update', 'id' => 1], ['UserForm[name]' => 'G.Hopper']);
* ```
*
* ## Status
*
* Maintainer: **samdark**
* Stability: **stable**
*
*/
class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule
{
/**
* Application config file must be set.
* @var array
*/
protected array $config = [
'fixturesMethod' => '_fixtures',
'cleanup' => true,
'ignoreCollidingDSN' => false,
'transaction' => true,
'entryScript' => '',
'entryUrl' => 'http://localhost/index-test.php',
'responseCleanMethod' => Yii2Connector::CLEAN_CLEAR,
'requestCleanMethod' => Yii2Connector::CLEAN_RECREATE,
'recreateComponents' => [],
'recreateApplication' => false,
'closeSessionOnRecreateApplication' => true,
'applicationClass' => null,
];
protected array $requiredFields = ['configFile'];
/**
* @var Yii2Connector\FixturesStore[]
*/
public array $loadedFixtures = [];
/**
* Helper to manage database connections
*/
private ConnectionWatcher $connectionWatcher;
/**
* Helper to force database transaction
*/
private TransactionForcer $transactionForcer;
/**
* @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.
*/
private array $server;
private Logger $yiiLogger;
private function getClient(): \Codeception\Lib\Connector\Yii2|null
{
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;
}
public function _initialize(): void
{
if ($this->config['transaction'] === null) {
$this->config['transaction'] = $this->backupConfig['transaction'] = $this->config['cleanup'];
}
$this->defineConstants();
$this->server = $_SERVER;
$this->initServerGlobal();
}
/**
* Module configuration changed inside a test.
* We always re-create the application.
*/
protected function onReconfigure(): void
{
parent::onReconfigure();
$this->getClient()->resetApplication();
$this->configureClient($this->config);
$this->yiiLogger->getAndClearLog();
$this->getClient()->startApp($this->yiiLogger);
}
/**
* Adds the required server params.
* Note this is done separately from the request cycle since someone might call
* `Url::to` before doing a request, which would instantiate the request component with incorrect server params.
*/
private function initServerGlobal(): void
{
$entryUrl = $this->config['entryUrl'];
$entryFile = $this->config['entryScript'] ?: basename($entryUrl);
$entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_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'
]);
}
protected function validateConfig(): void
{
parent::validateConfig();
$pathToConfig = codecept_absolute_path($this->config['configFile']);
if (!is_file($pathToConfig)) {
throw new ModuleConfigException(
__CLASS__,
"The application config file does not exist: " . $pathToConfig
);
}
if (!in_array($this->config['responseCleanMethod'], Yii2Connector::CLEAN_METHODS)) {
throw new ModuleConfigException(
__CLASS__,
"The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS)
);
}
if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS)) {
throw new ModuleConfigException(
__CLASS__,
"The response clean method must be one of: " . implode(", ", Yii2Connector::CLEAN_METHODS)
);
}
}
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;
}
}
$this->getClient()->resetApplication();
}
/**
* Instantiates the client based on module configuration
*/
protected function recreateClient(): void
{
$entryUrl = $this->config['entryUrl'];
$entryFile = $this->config['entryScript'] ?: basename($entryUrl);
$entryScript = $this->config['entryScript'] ?: parse_url($entryUrl, PHP_URL_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'
]);
$this->configureClient($this->config);
}
public function _before(TestInterface $test): void
{
$this->recreateClient();
$this->yiiLogger = new Yii2Connector\Logger();
$this->getClient()->startApp($this->yiiLogger);
$this->connectionWatcher = new ConnectionWatcher();
$this->connectionWatcher->start();
// load fixtures before db transaction
if ($test instanceof \Codeception\Test\Cest) {
$this->loadFixtures($test->getTestInstance());
} elseif ($test instanceof \Codeception\Test\TestCaseWrapper) {
$this->loadFixtures($test->getTestCase());
} else {
$this->loadFixtures($test);
}
$this->startTransactions();
}
/**
* load fixtures before db transaction
*/
private function loadFixtures(object $test): void
{
$this->debugSection('Fixtures', 'Loading fixtures');
if (empty($this->loadedFixtures)
&& method_exists($test, $this->_getConfig('fixturesMethod'))
) {
$connectionWatcher = new ConnectionWatcher();
$connectionWatcher->start();
$this->haveFixtures(call_user_func([$test, $this->_getConfig('fixturesMethod')]));
$connectionWatcher->stop();
$connectionWatcher->closeAll();
}
$this->debugSection('Fixtures', 'Done');
}
public function _after(TestInterface $test): void
{
$_SESSION = [];
$_FILES = [];
$_GET = [];
$_POST = [];
$_COOKIE = [];
$_REQUEST = [];
$this->rollbackTransactions();
if ($this->config['cleanup']) {
foreach ($this->loadedFixtures as $fixture) {
$fixture->unloadFixtures();
}
$this->loadedFixtures = [];
}
$this->getClient()?->resetApplication();
if (isset($this->connectionWatcher)) {
$this->connectionWatcher->stop();
$this->connectionWatcher->closeAll();
unset($this->connectionWatcher);
}
parent::_after($test);
}
public function _failed(TestInterface $test, $fail): void
{
$log = $this->yiiLogger->getAndClearLog();
if ($log !== '') {
$test->getMetadata()->addReport('yii-log', $log);
}
parent::_failed($test, $fail);
}
protected function startTransactions(): void
{
if ($this->config['transaction']) {
$this->transactionForcer = new TransactionForcer($this->config['ignoreCollidingDSN']);
$this->transactionForcer->start();
}
}
protected function rollbackTransactions(): void
{
if (isset($this->transactionForcer)) {
$this->transactionForcer->rollbackAll();
$this->transactionForcer->stop();
unset($this->transactionForcer);
}
}
public function _parts(): array
{
return ['orm', 'init', 'fixtures', 'email'];
}
/**
* Authenticates a user on a site without submitting a login form.
* Use it for fast pragmatic authorization in functional tests.
*
* ```php
* <?php
* // User is found by id
* $I->amLoggedInAs(1);
*
* // User object is passed as parameter
* $admin = \app\models\User::findByUsername('admin');
* $I->amLoggedInAs($admin);
* ```
* Requires the `user` component to be enabled and configured.
*
* @throws \Codeception\Exception\ModuleException
*/
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) {
throw new ModuleException($this, $e->getMessage());
}
}
/**
* Creates and loads fixtures from a config.
* The signature is the same as for the `fixtures()` method of `yii\test\FixtureTrait`
*
* ```php
* <?php
* $I->haveFixtures([
* 'posts' => PostsFixture::class,
* 'user' => [
* 'class' => UserFixture::class,
* 'dataFile' => '@tests/_data/models/user.php',
* ],
* ]);
* ```
*
* Note: if you need to load fixtures before a test (probably before the
* cleanup transaction is started; `cleanup` option is `true` by default),
* you can specify the fixtures in the `_fixtures()` method of a test case
*
* ```php
* <?php
* // inside Cest file or Codeception\TestCase\Unit
* public function _fixtures(){
* return [
* 'user' => [
* 'class' => UserFixture::class,
* 'dataFile' => codecept_data_dir() . 'user.php'
* ]
* ];
* }
* ```
* instead of calling `haveFixtures` in Cest `_before`
*
* @param $fixtures
* @part fixtures
*/
public function haveFixtures($fixtures): void
{
if (empty($fixtures)) {
return;
}
$fixturesStore = new Yii2Connector\FixturesStore($fixtures);
$fixturesStore->unloadFixtures();
$fixturesStore->loadFixtures();
$this->loadedFixtures[] = $fixturesStore;
}
/**
* Returns all loaded fixtures.
* Array of fixture instances
*
* @part fixtures
* @return array
*/
public function grabFixtures()
{
if (!$this->loadedFixtures) {
return [];
}
return call_user_func_array(
'array_merge',
array_map( // merge all fixtures from all fixture stores
function ($fixturesStore) {
return $fixturesStore->getFixtures();
},
$this->loadedFixtures
)
);
}
/**
* Gets a fixture by name.
* Returns a Fixture instance. If a fixture is an instance of
* `\yii\test\BaseActiveFixture` a second parameter can be used to return a
* specific model:
*
* ```php
* <?php
* $I->haveFixtures(['users' => UserFixture::class]);
*
* $users = $I->grabFixture('users');
*
* // get first user by key, if a fixture is an instance of ActiveFixture
* $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)
{
$fixtures = $this->grabFixtures();
if (!isset($fixtures[$name])) {
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");
}
/**
* Inserts a record into the database.
*
* ``` php
* <?php
* $user_id = $I->haveRecord('app\models\User', array('name' => 'Davert'));
* ?>
* ```
* @template T of \yii\db\ActiveRecord
* @param class-string<T> $model
* @param array<string, mixed> $attributes
* @return mixed
* @part orm
*/
public function haveRecord(string $model, $attributes = []): mixed
{
/** @var T $record **/
$record = \Yii::createObject($model);
$record->setAttributes($attributes, false);
$res = $record->save(false);
if (!$res) {
$this->fail("Record $model was not saved: " . \yii\helpers\Json::encode($record->errors));
}
return $record->primaryKey;
}
/**
* Checks that a record exists in the database.
*
* ``` php
* $I->seeRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param class-string<\yii\db\ActiveRecord> $model
* @param array<string, mixed> $attributes
* @part orm
*/
public function seeRecord(string $model, array $attributes = []): void
{
$record = $this->findRecord($model, $attributes);
if (!$record) {
$this->fail("Couldn't find $model with " . json_encode($attributes));
}
$this->debugSection($model, json_encode($record));
}
/**
* Checks that a record does not exist in the database.
*
* ``` php
* $I->dontSeeRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param class-string<\yii\db\ActiveRecord> $model
* @param array<string, mixed> $attributes
* @part orm
*/
public function dontSeeRecord(string $model, array $attributes = []): void
{
$record = $this->findRecord($model, $attributes);
$this->debugSection($model, json_encode($record));
if ($record) {
$this->fail("Unexpectedly managed to find $model with " . json_encode($attributes));
}
}
/**
* Retrieves a record from the database
*
* ``` php
* $category = $I->grabRecord('app\models\User', array('name' => 'davert'));
* ```
*
* @param class-string<\yii\db\ActiveRecord> $model
* @param array<string, mixed> $attributes
* @part orm
*/
public function grabRecord(string $model, array $attributes = []): \yii\db\ActiveRecord|null|array
{
return $this->findRecord($model, $attributes);
}
/**
* @param class-string<\yii\db\ActiveRecord> $model Class name
* @param array<string, mixed> $attributes
*/
protected function findRecord(string $model, array $attributes = []): \yii\db\ActiveRecord | null | array
{
if (!class_exists($model)) {
throw new \RuntimeException("Class $model does not exist");
}
$rc = new \ReflectionClass($model);
if ($rc->hasMethod('find')
/** @phpstan-ignore-next-line */
&& ($findMethod = $rc->getMethod('find'))
&& $findMethod->isStatic()
&& $findMethod->isPublic()
&& $findMethod->getNumberOfRequiredParameters() === 0
) {
$activeQuery = $findMethod->invoke(null);
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("Class $model does not have a public static find() method without required parameters");
}
/**
* Similar to `amOnPage` but accepts a route as first argument and params as second
*
* ```
* $I->amOnRoute('site/view', ['page' => 'about']);
* ```
*
* @param string $route A route
* @param array $params Additional route parameters
*/
public function amOnRoute(string $route, array $params = []): void
{
if (Yii::$app->controller === null) {
$route = "/{$route}";
}
array_unshift($params, $route);
$this->amOnPage(Url::to($params));
}
/**
* Gets a component from the Yii container. Throws an exception if the
* component is not available
*
* ```php
* <?php
* $mailer = $I->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.
*
* ```php
* <?php
* // check that at least 1 email was sent
* $I->seeEmailIsSent();
*
* // check that only 3 emails were sent
* $I->seeEmailIsSent(3);
* ```
*
* @param int|null $num
* @throws \Codeception\Exception\ModuleException
* @part email
*/
public function seeEmailIsSent(?int $num = null): void
{
if ($num === null) {
$this->assertNotEmpty($this->grabSentEmails(), 'emails were sent');
return;
}
$this->assertEquals($num, count($this->grabSentEmails()), 'number of sent emails is equal to ' . $num);
}
/**
* Checks that no email was sent
*
* @part email
*/
public function dontSeeEmailIsSent(): void
{
$this->seeEmailIsSent(0);
}
/**
* Returns array of all sent email messages.
* Each message implements the `yii\mail\MessageInterface` interface.
* Useful to perform additional checks using the `Asserts` module:
*
* ```php
* <?php
* $I->seeEmailIsSent();
* $messages = $I->grabSentEmails();
* $I->assertEquals('admin@site,com', $messages[0]->getTo());
* ```
*
* @part email
* @return array
* @throws \Codeception\Exception\ModuleException
*/
public function grabSentEmails(): array
{
try {
return $this->getClient()->getEmails();
} catch (ConfigurationException $e) {
throw new ModuleException($this, $e->getMessage());
}
}
/**
* Returns the last sent email:
*
* ```php
* <?php
* $I->seeEmailIsSent();
* $message = $I->grabLastSentEmail();
* $I->assertEquals('admin@site,com', $message->getTo());
* ```
* @part email
*/
public function grabLastSentEmail(): object
{
$this->seeEmailIsSent();
$messages = $this->grabSentEmails();
return end($messages);
}
/**
* Returns a list of regex patterns for recognized domain names
*
* @return array
*/
public function getInternalDomains(): array
{
return $this->getClient()->getInternalDomains();
}
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);
}
/**
* 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`.
*/
public function setCookie($name, $val, $params = [])
{
parent::setCookie($name, $this->getClient()->hashCookieData($name, $val), $params);
}
/**
* Creates the CSRF Cookie.
* @param string $val The value of the CSRF token
* @return string[] Returns an array containing the name of the CSRF param and the masked CSRF token.
*/
public function createAndSetCsrfCookie(string $val): array
{
$masked = (new Security())->maskToken($val);
$name = $this->getClient()->getCsrfParamName();
$this->setCookie($name, $val);
return [$name, $masked];
}
public function _afterSuite(): void
{
parent::_afterSuite();
codecept_debug('Suite done, restoring $_SERVER to original');
$_SERVER = $this->server;
}
/**
* Initialize an empty session. Implements MultiSession.
*/
public function _initializeSession(): void
{
$this->getClient()->restart();
$this->headers = [];
$_SESSION = [];
$_COOKIE = [];
}
/**
* Return the session content for future restoring. Implements MultiSession.
* @return array<string, mixed> backup data
*/
public function _backupSession(): array
{
if (Yii::$app instanceof Application && Yii::$app->has('session', true) && Yii::$app->session->useCustomStorage) {
throw new ModuleException($this, "Yii2 MultiSession only supports the default session backend.");
}
return [
'clientContext' => $this->getClient()->getContext(),
'headers' => $this->headers,
'cookie' => isset($_COOKIE) ? $_COOKIE : [],
'session' => isset($_SESSION) ? $_SESSION : [],
];
}
/**
* Restore a session. Implements MultiSession.
* @param array<mixed> $session output of _backupSession()
*/
public function _loadSession($session): void
{
$this->getClient()->setContext($session['clientContext']);
$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']);
}
}
}
/**
* Close and dump a session. Implements MultiSession.
*/
public function _closeSession($session = null): void
{
if (!$session) {
$this->_initializeSession();
}
}
}