2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: SessionUser::readSession called with session_id: okga03db4vutboqfsp4aao9rhn; Context: {
    "session ID": "okga03db4vutboqfsp4aao9rhn",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::readSession",
    "function": "readSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 243
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Session ID: okga03db4vutboqfsp4aao9rhn changed, clearing cached session tokens.; Context: {
    "old_session_id": null,
    "new_session_id": "okga03db4vutboqfsp4aao9rhn",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSessionToken",
    "function": "getSessionToken",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1101
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Querying SessionUser DB record for session ID: okga03db4vutboqfsp4aao9rhn; Context: {
    "session_id": "okga03db4vutboqfsp4aao9rhn",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSession",
    "function": "getSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1029
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: SessionUser DB record found for: okga03db4vutboqfsp4aao9rhn; Context: {
    "user session_id": "okga03db4vutboqfsp4aao9rhn",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSession",
    "function": "getSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1060
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Session data fetched from Yii session storage for key: __sessionUser::cjefjv89bmcp_Kp_9sutjizxrWv8RFNE; Context: {
    "key": "__sessionUser::cjefjv89bmcp_Kp_9sutjizxrWv8RFNE",
    "session manager data": "{\"session_id\":\"okga03db4vutboqfsp4aao9rhn\",\"session_token\":\"cjefjv89bmcp_Kp_9sutjizxrWv8RFNEK8ohqPUTB_biahZjtz6LRrhIFGWxxAZs\",\"user_ip\":\"::1\",\"user_agent\":\"Mozilla\\\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\\\/20100101 Firefox\\\/128.0\",\"extra_data\":[],\"cart_data\":[],\"expires_at\":1750705714}",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::fetchSessionData",
    "function": "fetchSessionData",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 335
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Stored IP: , Current IP: ::1; Context: {
    "stored user IP": "::1",
    "current IP": "::1"
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Stored IP: , Current IP: ::1; Context: {
    "stored user_agent": "Mozilla\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\/20100101 Firefox\/128.0",
    "current user_agent": "Mozilla\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\/20100101 Firefox\/128.0"
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: SessionUser::readSession called with session_id: okga03db4vutboqfsp4aao9rhn; Context: {
    "session ID": "okga03db4vutboqfsp4aao9rhn",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::readSession",
    "function": "readSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 243
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Session data fetched from Yii session storage for key: __sessionUser::cjefjv89bmcp_Kp_9sutjizxrWv8RFNE; Context: {
    "key": "__sessionUser::cjefjv89bmcp_Kp_9sutjizxrWv8RFNE",
    "session manager data": "{\"session_id\":\"okga03db4vutboqfsp4aao9rhn\",\"session_token\":\"cjefjv89bmcp_Kp_9sutjizxrWv8RFNEK8ohqPUTB_biahZjtz6LRrhIFGWxxAZs\",\"user_ip\":\"::1\",\"user_agent\":\"Mozilla\\\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\\\/20100101 Firefox\\\/128.0\",\"extra_data\":[],\"cart_data\":[],\"expires_at\":1750705714}",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::fetchSessionData",
    "function": "fetchSessionData",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 335
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Stored IP: , Current IP: ::1; Context: {
    "stored user IP": "::1",
    "current IP": "::1"
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Stored IP: , Current IP: ::1; Context: {
    "stored user_agent": "Mozilla\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\/20100101 Firefox\/128.0",
    "current user_agent": "Mozilla\/5.0 (X11; Linux x86_64; rv:128.0) Gecko\/20100101 Firefox\/128.0"
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: validateJsonData called with keys: last_login_time; Context: {
    "json data": "last_login_time",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::validateJsonData",
    "function": "validateJsonData",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 635
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Session ID: 1bfqjdi5gebsmfok0res0bu644 changed, clearing cached session tokens.; Context: {
    "old_session_id": "okga03db4vutboqfsp4aao9rhn",
    "new_session_id": "1bfqjdi5gebsmfok0res0bu644",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSessionToken",
    "function": "getSessionToken",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1101
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:21 [::1][1][-][info][application] $_SERVER = [
    'DOCUMENT_ROOT' => '/home/mort/www/yii-ladlite/web'
    'REMOTE_ADDR' => '::1'
    'REMOTE_PORT' => '33674'
    'SERVER_SOFTWARE' => 'PHP 8.2.28 Development Server'
    'SERVER_PROTOCOL' => 'HTTP/1.1'
    'SERVER_NAME' => 'localhost'
    'SERVER_PORT' => '8080'
    'REQUEST_URI' => '/index.php?r=site%2Flogin'
    'REQUEST_METHOD' => 'POST'
    'SCRIPT_NAME' => '/index.php'
    'SCRIPT_FILENAME' => '/home/mort/www/yii-ladlite/web/index.php'
    'PHP_SELF' => '/index.php'
    'QUERY_STRING' => 'r=site%2Flogin'
    'HTTP_HOST' => 'localhost:8080'
    'HTTP_USER_AGENT' => ''
    'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.5'
    'HTTP_ACCEPT_ENCODING' => 'gzip, deflate, br, zstd'
    'CONTENT_TYPE' => 'application/x-www-form-urlencoded'
    'HTTP_CONTENT_TYPE' => 'application/x-www-form-urlencoded'
    'CONTENT_LENGTH' => '269'
    'HTTP_CONTENT_LENGTH' => '269'
    'HTTP_ORIGIN' => 'http://localhost:8080'
    'HTTP_CONNECTION' => 'keep-alive'
    'HTTP_REFERER' => 'http://localhost:8080/index.php?r=site%2Flogin'
    'HTTP_COOKIE' => 'PHPSESSID=okga03db4vutboqfsp4aao9rhn; _csrf=f3d5efb2c03d91a3f47243f1bec550b049d0c26d2ddca94185e2472361f017b8a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22tDJDIICHvS83fsxiv7rKxhEmx23c4eYK%22%3B%7D'
    'HTTP_UPGRADE_INSECURE_REQUESTS' => '1'
    'HTTP_SEC_FETCH_DEST' => 'document'
    'HTTP_SEC_FETCH_MODE' => 'navigate'
    'HTTP_SEC_FETCH_SITE' => 'same-origin'
    'HTTP_SEC_FETCH_USER' => '?1'
    'HTTP_DNT' => '1'
    'HTTP_SEC_GPC' => '1'
    'HTTP_PRIORITY' => 'u=0, i'
    'REQUEST_TIME_FLOAT' => 1750702161.9714
    'REQUEST_TIME' => 1750702161
]
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: SessionUser::readSession called with session_id: 1bfqjdi5gebsmfok0res0bu644; Context: {
    "session ID": "1bfqjdi5gebsmfok0res0bu644",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::readSession",
    "function": "readSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 243
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Session ID: 1bfqjdi5gebsmfok0res0bu644 changed, clearing cached session tokens.; Context: {
    "old_session_id": null,
    "new_session_id": "1bfqjdi5gebsmfok0res0bu644",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSessionToken",
    "function": "getSessionToken",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1101
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][error][application] DEBUG MODE: Querying SessionUser DB record for session ID: 1bfqjdi5gebsmfok0res0bu644; Context: {
    "session_id": "1bfqjdi5gebsmfok0res0bu644",
    "class": "app\\components\\services\\SessionUserService",
    "method": "app\\components\\services\\SessionUserService::getSession",
    "function": "getSession",
    "file": "\/home\/\/components\/services\/SessionUserService.php",
    "line": 1029
}
    in /home/mort/www/yii-ladlite/components/AppLogger.php:272
    in /home/mort/www/yii-ladlite/components/AppLogger.php:144
    in /home/mort/www/yii-ladlite/components/AppLogger.php:185
2025-06-23 18:09:22 [::1][1][-][info][application] $_SERVER = [
    'DOCUMENT_ROOT' => '/home/mort/www/yii-ladlite/web'
    'REMOTE_ADDR' => '::1'
    'REMOTE_PORT' => '33678'
    'SERVER_SOFTWARE' => 'PHP 8.2.28 Development Server'
    'SERVER_PROTOCOL' => 'HTTP/1.1'
    'SERVER_NAME' => 'localhost'
    'SERVER_PORT' => '8080'
    'REQUEST_URI' => '/'
    'REQUEST_METHOD' => 'GET'
    'SCRIPT_NAME' => '/index.php'
    'SCRIPT_FILENAME' => '/home/mort/www/yii-ladlite/web/index.php'
    'PHP_SELF' => '/index.php'
    'HTTP_HOST' => 'localhost:8080'
    'HTTP_USER_AGENT' => ''
    'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
    'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.5'
    'HTTP_ACCEPT_ENCODING' => 'gzip, deflate, br, zstd'
    'HTTP_REFERER' => 'http://localhost:8080/index.php?r=site%2Flogin'
    'HTTP_CONNECTION' => 'keep-alive'
    'HTTP_COOKIE' => 'PHPSESSID=1bfqjdi5gebsmfok0res0bu644; _csrf=52b1566a8246830fe4b28ab06cd7540065be8879bdb85486f3daaef5c2f4be60a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22OCWtX0rajPdK5yruSWJqBkf16AQeVLg5%22%3B%7D; _identity=5dfecee0b7215311b168290986e13e0f33c8520c3ebb72711a8cc1536150eb72a%3A2%3A%7Bi%3A0%3Bs%3A9%3A%22_identity%22%3Bi%3A1%3Bs%3A46%3A%22%5B1%2C%22jXH2B6Gvo4zMBYOIOBLEgwVt6Ch1X8pw%22%2C2592000%5D%22%3B%7D'
    'HTTP_UPGRADE_INSECURE_REQUESTS' => '1'
    'HTTP_SEC_FETCH_DEST' => 'document'
    'HTTP_SEC_FETCH_MODE' => 'navigate'
    'HTTP_SEC_FETCH_SITE' => 'same-origin'
    'HTTP_SEC_FETCH_USER' => '?1'
    'HTTP_DNT' => '1'
    'HTTP_SEC_GPC' => '1'
    'HTTP_PRIORITY' => 'u=0, i'
    'REQUEST_TIME_FLOAT' => 1750702162.5651
    'REQUEST_TIME' => 1750702162
]

```
namespace app\components\services;

// PHP built-in exception:
use \Throwable;

use Yii;
use yii\db\Exception as YiiDbException;

use app\components\SessionManager;
use app\components\interfaces\SessionInterface;
use app\models\SessionUser;
use app\helpers\ExceptionLogger;
use app\helpers\LogHelper;
use app\helpers\RoleHelper;

/**
* Session User Service
*/
class SessionUserService
{
	private bool $_debugMode = true;

	private const SESSION_TOKEN_LENGTH = 32;
	private const USER_AGENT_COMPARE_LENGTH = 30;
	
	private SessionInterface $_sessionManager;
	
	private ?SessionUser $_cachedSessionUser = null;
	
	private ?string $_currentSessionId = null;
	
	private array $_cachedSessionTokens = [];
	
	
	public function __construct(?SessionInterface $sessionManager = null)
	{
		$this->_sessionManager = $sessionManager ?? new SessionManager();
	}
	
	public function hasSession(): bool
	{
		$sessionId = $this->_sessionManager->getId();
		if (empty($sessionId))
		{
			return false;
		}
		
		$sessionToken = $this->getSessionToken($sessionId);
		if ($sessionToken === null)
		{
			return false;
		}
		
		$key = SessionUser::SESSION_USER_KEY_PREFIX . substr($sessionToken, 0, self::SESSION_TOKEN_LENGTH);
		
		return $this->_sessionManager->has($key);
	}
	
	public function writeSession(array $data = [], bool $allowCriticalFieldUpdate = false): bool
	{
		//$this->_sessionManager->destroy();// Debugging
		
		// If $data array is empty, theirs nothing to write, return false.
		if (empty($data))
		{
			LogHelper::debug('Attempted to write empty session data.');
			
			return false;
		}
		
		// Validate 'extra_data' if it exists with validateJsonData().
		if (isset($data['extra_data']) && !$this->validateJsonData($data['extra_data']))
		{
			if ($this->_debugMode)
			{
				LogHelper::error("Invalid extra_data JSON structure: " . json_encode($data['extra_data']), [
					'extra data' => $data['extra_data'],
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return false;
		}
		
		// Validate 'cart_data' if it exists with validateCartData().
		if (isset($data['cart_data']) && !$this->validateCartData($data['cart_data']))
		{
			if ($this->_debugMode)
			{
				LogHelper::error("Invalid extra_data JSON structure: " . json_encode($data['cart_data']), [
					'cart data' => $data['cart_data'],
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return false;
		}
		
		// Retrieve the session ID and session token.
		$sessionId = $this->_sessionManager->getId();
		
		if (empty($sessionId))
		{
			if ($this->_debugMode)
			{
				LogHelper::debug('No session ID available during writeSession.');
			}
			
			return false;
		}
		
		// Fetch existing token or generate a new one
		$sessionToken = $this->getSessionToken($sessionId);
		
		if ($sessionToken === null)
		{
			$sessionToken = $this->generateSessionToken();
		}
		
		// Remove old session key if token changed or was present
		$oldToken = $this->_cachedSessionTokens[$sessionId] ?? null;
		if ($oldToken !== null && $oldToken !== $sessionToken)
		{
			$oldKey = SessionUser::SESSION_USER_KEY_PREFIX . substr($oldToken, 0, self::SESSION_TOKEN_LENGTH);
			$this->_sessionManager->remove($oldKey);
		}
		
		// Update cached token
		$this->_cachedSessionTokens[$sessionId] = $sessionToken;
		
		$sessionTokenKey = substr($sessionToken, 0, self::SESSION_TOKEN_LENGTH);
		try
		{
			// Prepare data to be stored in Yii session and DB
			$sessionData = $this->initializeSessionData($sessionId, $sessionToken, $data);
			
			// Store session data in Yii session storage using canonical key
			$this->_sessionManager->set(SessionUser::SESSION_USER_KEY_PREFIX . $sessionTokenKey, $sessionData);
			
			// Save or update session data in DB
			return $this->saveSession($sessionId, $sessionData, $allowCriticalFieldUpdate);
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception.
			ExceptionLogger::log($e, 'sessionUser_writeSession_error_');
			
			return false;
		}
	}
	
	public function readSession(): ?array
	{
		//$this->_sessionManager->destroy();// Debugging
		
		$sessionId = $this->_sessionManager->getId();
		
		if ($this->_debugMode)
		{
			LogHelper::debug("SessionUser::readSession called with session_id: {$sessionId}", [
				'session ID' => $sessionId,
				'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__
			]);
		}
		
		if (empty($sessionId))
		{
			return null;
		}
		
		$sessionToken = $this->getSessionToken($sessionId);
		if ($sessionToken === null)
		{
			return null;
		}
		
		// Subtract x amount from $sessionToken for validateSessionData() mechanism.
		$sessionTokenKey = substr($sessionToken, 0, self::SESSION_TOKEN_LENGTH);
		
		// Read session data from session storage (Yii)
		$sessionData = $this->fetchSessionData($sessionTokenKey);
		if ($sessionData === null)
		{
			return null;
		}
		
		// Get session record in DB
		$sessionUser = $this->getSession($sessionId);
			
		// Validate token match, IP/user-agent, expiration...
		if ($sessionUser !== null && !$sessionUser->isExpired() && 
			$this->validateSessionData($sessionData, $sessionUser) &&
			$this->validateRequest($sessionUser)
		)
		{
			return $sessionData;
		}
		
		if ($this->_debugMode)
		{
			LogHelper::debug("Session validation failed for session_id: {$sessionId}", [
				'session ID' => $sessionId,
				'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__
			]);
		}
			
		return null;
	}
	
	private function fetchSessionData(string $sessionTokenKey): ?array
	{
		$key = SessionUser::SESSION_USER_KEY_PREFIX . $sessionTokenKey;
		try
		{
			if (!$this->_sessionManager->has($key))
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Session key: {$key} not found in Yii session storage.", [
						'key' => $key,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return null;
			}
			
			$data = $this->_sessionManager->get($key);
			
			if ($this->_debugMode)
			{
				LogHelper::debug("Session data fetched from Yii session storage for key: {$key}", [
					'key' => $key,
					'session manager data' => json_encode($data),
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return $data;
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception.
			ExceptionLogger::log($e, 'sessionUser_fetchSessionData_error_');
		}
		
		return null;
	}
	
	private function saveSession(
		string $sessionId,
		array $sessionData,
		bool $allowCriticalFieldUpdate = false
	): bool
	{
		try
		{
			$sessionUser = $this->getSession($sessionId);
			
			if ($sessionUser === null)
			{
				// Create new SessionUser record if missing
				$sessionUser = new SessionUser();
				$sessionUser->session_id = $sessionId;
				$sessionUser->session_token = $sessionData['session_token'];
				$sessionUser->user_ip = $sessionData['user_ip'];
				$sessionUser->user_agent = $sessionData['user_agent'];
				$sessionUser->extra_data = json_encode($sessionData['extra_data'] ?? [], JSON_THROW_ON_ERROR);
				$sessionUser->cart_data = json_encode($sessionData['cart_data'] ?? [], JSON_THROW_ON_ERROR);
				$sessionUser->expires_at = $sessionData['expires_at'] ?? null;
				
				if (!$sessionUser->save(false))
				{
					throw new YiiDbException('Failed to create new SessionUser DB record.');
				}
			}
			else
			{
				$updated = false;
				
				if ($allowCriticalFieldUpdate && $sessionUser->session_id !== null)
				{
					foreach (['session_token', 'user_ip', 'user_agent'] as $field)
					{
						if (isset($sessionData[$field]) && $sessionUser->{$field} !== $sessionData[$field])
						{
							$sessionUser->{$field} = $sessionData[$field];
							$updated = true;
						}
					}
				}
				
				$sessionUser->session_token = $sessionData['session_token'];
				$sessionUser->user_ip = $sessionData['user_ip'];
				$sessionUser->user_agent = $sessionData['user_agent'];
				$sessionUser->extra_data = json_encode($sessionData['extra_data'] ?? [], JSON_THROW_ON_ERROR);
				$sessionUser->cart_data = json_encode($sessionData['cart_data'] ?? [], JSON_THROW_ON_ERROR);
				$sessionUser->expires_at = $sessionData['expires_at'] ?? null;
				
				if ($updated || $sessionUser->isNewRecord)
				{
					if (!$sessionUser->save(false))
					{
						throw new YiiDbException('Failed to update user session in the database.');
					}
				}
			}
			
			// Update caches
			$this->_cachedSessionUser = $sessionUser;
			$this->_cachedSessionTokens[$sessionId] = $sessionUser->session_token;
			
			return true;
		}
		catch (Throwable $e)
		{
			ExceptionLogger::log($e, 'sessionUser_saveSession_error_');
		}
		
		return false;
	}
	
	private function initializeSessionData(string $sessionId, string $sessionToken, array $data = []): array
	{
		return [
			'session_id' => $sessionId,
			'session_token' => $sessionToken,
			'user_ip' => RoleHelper::userIp(),
			'user_agent' => RoleHelper::userAgent(),
			'extra_data' => array_merge($data['extra_data'] ?? [], [
				// Add any new default values you want to merge into extra_data here.
				// Example: 'additional_key' => 'additional_value',
			]),
			'cart_data' => $data['cart_data'] ?? [],// Accessing directly from the input.
			'expires_at' => time() + SessionUser::SESSION_DURATION_SECONDS,
		];
	}
	
	public function validateRequest(SessionUser $sessionUser): bool
	{
		$currentIp = RoleHelper::userIp();
		$storedIp = $sessionUser->user_ip;
		$storedAgent = $sessionUser->user_agent ?? '';
		$currentAgent = RoleHelper::userAgent() ?? '';
		//$storedAgent = $this->normalizeUserAgent($sessionUser->user_agent ?? '');
		//$currentAgent = $this->normalizeUserAgent(RoleHelper::userAgent() ?? '');
		
		if ($this->_debugMode)
		{
			LogHelper::debug("Stored IP: {$storedAgent}, Current IP: {$currentIp}", [
				'stored user IP' => $storedIp,
				'current IP' => $currentIp,
				/*'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__*/
			]);
			
			LogHelper::debug("Stored IP: {$storedAgent}, Current IP: {$currentIp}", [
				'stored user_agent' => $storedAgent,
				'current user_agent' => $currentAgent
			]);
		}
		
		if (!$this->ipMatches($storedIp, $currentIp))
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("IP mismatch for session {$sessionUser->session_id}." , [
					'session' => $sessionUser->session_id,
					'stored user IP' => $storedIp,
					'current IP' => $currentIp
				]);
				
				LogHelper::debug("IP mismatch for session {$sessionUser->session_id}." , [
					'session' => $sessionUser->session_id,
					'stored user IP' => $storedIp,
					'current IP' => $currentIp,
				]);
			}
			
			return false;
		}
		
		$storedAgent = $sessionUser->user_agent ?? '';
		
		// Simplified example: compare only first 30 character
		if (strncmp($storedAgent, $currentAgent, self::USER_AGENT_COMPARE_LENGTH) !== 0)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("User-Agent mismatch for session {$sessionUser->session_id}." , [
					'session' => $sessionUser->session_id,
					'stored user_agent' => $storedAgent,
					'current user_agent' => $currentAgent,
					'stored agent prefix 30' => substr($storedAgent, 0, 30),
					'current agent prefix 30' => substr($currentAgent, 0, 30)
				]);
			}
			
			return false;
		}
		
		return true;
	}
	
	private function normalizeUserAgent(string $ua): string
	{
		// Convert to lowercase for case-insensitive comparison
		$uaClean = strtolower($ua);
		
		/**
		* Optionally remove digits but keep spaces and letters (tune based on your needs)
		* Commented out as versions can matter:
		*
		$uaClean = preg_replace('/[0-9]/', '', $uaClean);//*/
		
		/**
		* Remove excessive whitespaces and normalize spaces
		*/
		$uaClean = preg_replace('/\s+/', ' ', $uaClean);//*/
		
		/**
		* Remove common punctuation that may vary but is insignificant
		* Adjust to allow meaningful chars like dots and slashes if you want
		*
		$uaClean = preg_replace('/[\,;\(\)\[\]]/', '', $uaClean);//*/
		
		// Trim leading/trailing spaces
		$uaClean = trim($uaClean);
		
		return $uaClean;
	}
	
	private function ipMatches(string $storedIp, string $currentIp): bool
	{
		// IPv4 subnet matching example
		if (filter_var($storedIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) 
			&& filter_var($currentIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
		)
		{
			return substr($storedIp, 0, strrpos($storedIp, '.')) === substr($currentIp, 0, strrpos($currentIp, '.'));
		}
		
		// Example IPv6 /64 comparison (first 4 blocks)
		if (filter_var($storedIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
			&& filter_var($currentIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
		)
		{
			$storedParts = explode(':', $storedIp);
			$currentParts = explode(':', $currentIp);
			
			return array_slice($storedParts, 0, 4) === array_slice($currentParts, 0, 4);
		}
		
		// Strict equality fallback for IPv6 or others
		return $storedIp === $currentIp;
	}
	
	private function validateJsonData(array $jsonData): bool
	{
		if ($this->_debugMode)
		{
			LogHelper::debug("validateJsonData called with keys: " . implode(', ', array_keys($jsonData)), [
				'json data' => implode(', ', array_keys($jsonData)),
				'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__
			]);
		}
		
		// TODO Define allowed keys for 'extra_data'.
		$allowedKeys = ['last_login_time', 'order_id', 'order_success_url', 'order_canceled_url'];
		
		// Restrict the number of entries in 'extra_data'.
		if (count($jsonData) > 10)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug('extra_data exceeds maximum number of keys: ' . count($jsonData), [
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return false;
		}
		
		foreach ($jsonData as $key => $value)
		{
			// Reject unexpected keys strictly
			if (!is_string($key) || !in_array($key, $allowedKeys, true))
			{
				LogHelper::debug("Invalid extra_data key found: {$key}", [
					'key' => $key,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
				
				return false;
			}
			
			// Reject nested values (arrays/objects not allowed)
			if (is_array($value) || is_object($value))
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("extra_data value for key {$key} is a nested array/object which is not allowed.", [
						'value type' => gettype($value),
						'key' => $key,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return false;
			}
			
			// Accept only string or int types
			if (!is_string($value) && !is_int($value))
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Invalid type for extra_data key {$key}. Expected string or int, got " 
						. gettype($value), 
						[
							'value type' => gettype($value),
							'key' => $key,
							'class' => __CLASS__,
							'method' => __METHOD__,
							'function' => __FUNCTION__,
							'file' => __FILE__,
							'line' => __LINE__
						]
					);
				}
				
				return false;
			}
			
			// Validate string length.
			if (is_string($value) && mb_strlen($value) > 255)
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Value for key {$key} exceeds max length of 255 chars.", [
						'value' => $value,
						'key' => $key,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return false;
			}
		}
		
		// Optional: check total serialized length to prevent too large payload
		$encoded = json_encode($jsonData);
		
		if ($encoded === false || strlen($encoded) > 4096)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug('Encoded extra_data JSON too large or failed to encode: ' 
					. $encoded === false ? 0 : strlen($encoded), 
					[
						'length' => $encoded === false ? 0 : strlen($encoded),
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]
				);
			}
			
			return false;
		}//*/
		
		return true;
	}
	
	private function validateCartData(array $cartData): bool
	{
		// Configuration: max allowed items in cart
		$maxItems = 10;
		
		if (count($cartData) > $maxItems)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("Cart data exceeds maximum: {$maxItems} allowed items: " 
					. count($cartData), 
					[
						'count' => count($cartData),
						'max' => $maxItems,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]
				);
			}
			
			return false;
		}
		
		foreach ($cartData as $index => $item)
		{
			if (!is_array($item) || !isset($item['id'], $item['quantity']))
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Cart item at index {$index} missing required keys or invalid format.", [
						'item' => $item,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return false;
			}
			
			if (!is_int($item['id']) && !is_string($item['id']))
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Cart item 'id' at index {$index} must be int or string.", [
						'id' => $item['id'],
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return false;
			}
			
			$qty = $item['quantity'];
			
			// Validate quantity is positive integer or string of digits
			if (!((is_int($qty) && $qty > 0) || (is_string($qty) && ctype_digit($qty) && (int) $qty > 0))
			)
			{
				if ($this->_debugMode)
				{
					LogHelper::debug("Cart item 'quantity' at index {$index} must be a positive integer.", [
						'quantity' => $qty,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				
				return false;
			}
			
			// Optional: reject unexpected extra keys in cart items to avoid abuse
			$allowedItemKeys = ['id', 'quantity'];
			
			foreach ($item as $key => $value)
			{
				if (!in_array($key, $allowedItemKeys, true))
				{
					if ($this->_debugMode)
					{
						LogHelper::debug("Unexpected key {$key} in cart item at index {$index}.", [
							'item' => $item,
							'class' => __CLASS__,
							'method' => __METHOD__,
							'function' => __FUNCTION__,
							'file' => __FILE__,
							'line' => __LINE__
						]);
					}
					
					return false;
				}
				
				// Optionally reject nested arrays in item fields (no complex objects)
				if (is_array($value) || is_object($value))
				{
					if ($this->_debugMode)
					{
						LogHelper::debug("Cart item value for key {$key} at index {$index} is nested array/object.", 
							[
								'value type' => gettype($value),
								'item' => $item,
								'class' => __CLASS__,
								'method' => __METHOD__,
								'function' => __FUNCTION__,
								'file' => __FILE__,
								'line' => __LINE__
							]
						);
					}
					
					return false;
				}
			}//*/
		}
		
		// Optional: Check total serialized length of cart_data JSON to prevent too large carts
		$encoded = json_encode($cartData);
		
		if ($encoded === false || strlen($encoded) > 8192)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug('Encoded extra_data JSON too large or failed to encode: ' 
					. $encoded === false ? 0 : strlen($encoded), 
					[
						'length' => $encoded === false ? 0 : strlen($encoded),
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]
				);
			}
			
			return false;
		}//*/
		
		return true;
	}
	
	public function invalidate(string $sessionId): bool
	{
		$sessionUser = SessionUser::findOne(['session_id' => $sessionId]);
		
		if ($sessionUser === null)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("Attempted to invalidate non-existing session user with ID: {$sessionId}", [
					'user session_id' => $sessionId,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return false;
		}
		
		$result = $sessionUser->delete() !== false;
		
		if ($result)
		{
			$this->_cachedSessionUser = null;
			
			unset($this->_cachedSessionTokens[$sessionId]);
		}
		
		if ($this->_debugMode)
		{
			LogHelper::debug("Invalidate session with ID: {$sessionId}", [
				'session ID' => $sessionId,
				'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__
			]);
		}
		
		return $result;
	}
	
	private function getSession(string $sessionId): ?SessionUser
	{
		if ($this->_currentSessionId !== $sessionId)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug(
					"Session ID: {$this->_currentSessionId} changed, clearing cached SessionUser ID: {$sessionId}", 
					[
						'old_session_id' => $this->_currentSessionId,
						'new_session_id' => $sessionId,
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]
				);
			}
			
			$this->_cachedSessionUser = null;
			$this->_currentSessionId = $sessionId;
			
			/**
			* Also clear any related caches tied to session ID,
			* For example, cached token if you implement.
			*/
			unset($this->_cachedSessionTokens[$sessionId]);
		}
		
		if ($this->_cachedSessionUser !== null)
		{
			return $this->_cachedSessionUser;
		}	
		
		if ($this->_debugMode)
		{
			LogHelper::debug("Querying SessionUser DB record for session ID: {$sessionId}", [
				'session_id' => $sessionId,
				'class' => __CLASS__,
				'method' => __METHOD__,
				'function' => __FUNCTION__,
				'file' => __FILE__,
				'line' => __LINE__
			]);
		}
		
		// Get from SessionUser ActiveRecord
		$sessionUser = SessionUser::findOne(['session_id' => $sessionId]);
		
		if ($sessionUser === null)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("No SessionUser DB record found for: {$sessionUser->session_id}", [
					'user session_id' => $sessionUser->session_id,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
		}
		else
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("SessionUser DB record found for: {$sessionUser->session_id}", [
					'user session_id' => $sessionUser->session_id,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			$this->_cachedSessionUser = $sessionUser;
		}
		
		return $sessionUser;
	}
	
	private function generateSessionToken(): string
	{
		return \Yii::$app->security->generateRandomString(64);
	}
	
	private function getSessionToken(string $sessionId): ?string
	{
		if ($this->_currentSessionId !== $sessionId)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("Session ID: {$sessionId} changed, clearing cached session tokens.", [
					'old_session_id' => $this->_currentSessionId,
					'new_session_id' => $sessionId,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			// Clear token cache for old session ID
			unset($this->_cachedSessionTokens[$this->_currentSessionId]);
			
			$this->_currentSessionId = $sessionId;
		}
		
		if (isset($this->_cachedSessionTokens[$sessionId]))
		{
			return $this->_cachedSessionTokens[$sessionId];
		}
		
		$sessionUser = $this->getSession($sessionId);
		
		if ($sessionUser !== null && !empty($sessionUser->session_token))
		{
			$this->_cachedSessionTokens[$sessionId] = $sessionUser->session_token;
			
			return $sessionUser->session_token;
		}
		
		return null;
	}
	
	public function rollSessionToken(string $sessionId): bool
	{
		$sessionUser = $this->getSession($sessionId);
		
		if ($sessionUser === null)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("No SessionUser record found in rollSessionToken for id: {$sessionId}", [
					'session ID' => $sessionId,
					'class' => __CLASS__,
					'method' => __METHOD__,
					'function' => __FUNCTION__,
					'file' => __FILE__,
					'line' => __LINE__
				]);
			}
			
			return false;
		}
		
		$oldTokenKey = substr($sessionUser->session_token, 0, self::SESSION_TOKEN_LENGTH);
		$newToken = $this->generateSessionToken();
		$newTokenKey = substr($newToken, 0, self::SESSION_TOKEN_LENGTH);
		
		$oldSessionData = $this->_sessionManager->get(SessionUser::SESSION_USER_KEY_PREFIX . $oldTokenKey);
		if ($oldSessionData === null)
		{
			if ($this->_debugMode)
			{
				LogHelper::debug("No existing session data found during token roll.");
			}
			
			return false;
		}
		
		$oldSessionData['session_token'] = $newToken;
		
		// Remove old session key and save new
		$this->_sessionManager->remove(SessionUser::SESSION_USER_KEY_PREFIX . $oldTokenKey);
		$this->_sessionManager->set(SessionUser::SESSION_USER_KEY_PREFIX . $newTokenKey, $oldSessionData);
		
		try
		{
			$sessionUser->session_token = $newToken;
			if (!$sessionUser->save(false, ['session_token']))
			{
				throw new YiiDbException("Failed to save session token to the database.");
			}
			
			return true;
		}
		catch (Throwable $e)
		{
			// Use the ExceptionLogger to handle and log the exception.
			ExceptionLogger::log($e, 'session_user_service_rollSessionToken_error_');
		}
		
		return false;
	}
}
```


```
class BaseController extends Controller
{
	protected ?SessionUserService $_sessionUser = null;
	protected ?SessionCartService $_sessionCart = null;
	
	public function init()
	{
		parent::init();
		
		$this->_sessionUser = ServiceLocator::get('sessionUserService');
		$this->_sessionCart = ServiceLocator::get('sessionCartService');
		if ($this->_sessionUser === null xor $this->_sessionCart === null)
		{
			throw new RuntimeException("Failed to load SessionUserService xor SessionCartService from ServiceLocator");
		}
	}
	
	public function beforeAction($action): bool
	{
		if (!parent::beforeAction($action))
		{
			return false;
		}
		
		/**
		* Middleware Enforce Session Security
		*/
		$sessionId = \Yii::$app->session->getId();
		 
		 if ($this->_debugMode)
		 {
		 	LogHelper::debug('Session ID: ' . $sessionId, ['session ID' => $sessionId]);
		 }
		 
		 try
		 {
		 	$sessionData = $this->_sessionUser->readSession();
		 }
		 catch (Throwable $e)
		 {
		 	if ($this->_debugMode)
		 	{
		 		LogHelper::debug('Error reading session data: ' . $e->getMessage(), [
		 			'exception' => $e,
		 			'route' => \Yii::$app->requestedRoute
		 		]);
		 	}
		 	
		 	// Optionally handle error (for instance redirect or show maintenance)
		 	return false;
		 }
		 
		 if (!$sessionData)
		 {
		 	// Session invalid or expired - clean up session and related services
		 	$this->_sessionUser->invalidate($sessionId);
		 	$this->_sessionCart->clearCart();
		 	
		 	RoleHelper::userLogout(true);
		 	
		 	if ($this->_debugMode)
		 	{
		 		\Yii::$app->session->setFlash(
		 			'warning',
		 			\Yii::t('app', 'Your session has expired or was invalidated. Please log in again.')
		 		);
		 	}
		 	
		 	\Yii::$app->response->redirect(['site/login'])->send();
		 	
		 	return false;
		 }
		return true;
	}
}
```

```
class BaseController extends Controller
{
	protected ?SessionUserService $_sessionUser = null;
	protected ?SessionCartService $_sessionCart = null;
	
	public function init()
	{
		parent::init();
		
		$this->_sessionUser = ServiceLocator::get('sessionUserService');
		$this->_sessionCart = ServiceLocator::get('sessionCartService');
		if ($this->_sessionUser === null xor $this->_sessionCart === null)
		{
			throw new RuntimeException("Failed to load SessionUserService xor SessionCartService from ServiceLocator");
		}
	}
	
	public function actionLogout()
    {
       $userId = Yii::$app->user->id;
        $sessionId = Yii::$app->session->id;

$this->_sessionUser->writeSession([
					'extra_data' => ['last_login_time' => time()],
					// Optionally, you can pass existing cart_data to keep it synced.
				]);
	$this->_sessionCart->setCartSessionToDatabase($userId);

        if (!$sessionUserService->rollSessionToken($sessionId))
				{
					LogHelper::warning("Failed to roll session token after login", [
						'class' => __CLASS__,
						'method' => __METHOD__,
						'function' => __FUNCTION__,
						'file' => __FILE__,
						'line' => __LINE__
					]);
				}
				

        return $this->goHome();
    }
}
```
all I get is a blank page when attempint to login