Skip to content

Commit e1acdcc

Browse files
committed
refactor: bypass native PHP session functions in CoroutineSession
Replace parent::open()/close() calls with direct Redis operations to avoid "headers already sent" errors in Swoole environment. - Add $_sessionId property for internal session ID tracking - Override open/close/getId/setId/getIsActive methods - Manually serialize/deserialize session data to/from Redis
1 parent d02e35e commit e1acdcc

File tree

1 file changed

+57
-16
lines changed

1 file changed

+57
-16
lines changed

src/Session/CoroutineSession.php

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class CoroutineSession extends YiiRedisSession
1919
private bool $deferRegistered = false;
2020

2121
private int $deferCoroutineId = -1;
22+
23+
private ?string $_sessionId = null;
2224

2325
public function init()
2426
{
@@ -47,16 +49,30 @@ public function init()
4749

4850
public function open()
4951
{
50-
$this->isClosed = false;
52+
if ($this->getIsActive()) {
53+
return;
54+
}
5155

56+
$this->isClosed = false;
57+
5258
if ($this->autoCloseOnCoroutineEnd) {
5359
$this->registerCoroutineCloseHandler();
5460
}
5561

5662
$this->ensureSessionId();
5763

58-
parent::open();
59-
64+
// Load session data from Redis without calling parent::open()
65+
// to avoid session_set_save_handler() which fails after headers sent
66+
$data = $this->readSession($this->getId());
67+
68+
// Unserialize session data
69+
if (!empty($data)) {
70+
$_SESSION = @unserialize($data) ?: [];
71+
} else {
72+
$_SESSION = [];
73+
}
74+
75+
$this->updateFlashCounters();
6076
$this->ensureResponseCarriesSessionCookie();
6177
}
6278

@@ -72,23 +88,20 @@ public function close()
7288
return;
7389
}
7490

75-
// Check if session is actually active before attempting to close
76-
if (session_status() !== PHP_SESSION_ACTIVE) {
77-
// Session already closed at PHP level, just clean up resources
78-
if ($this->redis instanceof CoroutineRedisConnection) {
79-
$this->redis->close();
91+
// Save session data to Redis BEFORE marking as closed
92+
// This must happen before setting isClosed = true
93+
if (isset($_SESSION) && is_array($_SESSION) && !empty($_SESSION) && $this->_sessionId !== null) {
94+
try {
95+
// Serialize session data and write to Redis
96+
$data = serialize($_SESSION);
97+
$this->writeSession($this->getId(), $data);
98+
} catch (\Throwable $e) {
99+
\Yii::error('Failed to save session: ' . $e->getMessage(), __METHOD__);
80100
}
81-
$this->deferRegistered = false;
82-
$this->deferCoroutineId = -1;
83-
$this->isClosed = true;
84-
return;
85101
}
86102

103+
// NOW mark as closed
87104
$this->isClosed = true;
88-
89-
if ($this->getIsActive()) {
90-
parent::close();
91-
}
92105

93106
if ($this->redis instanceof CoroutineRedisConnection) {
94107
$this->redis->close();
@@ -106,10 +119,38 @@ public function reset(): void
106119
{
107120
$this->close();
108121
}
122+
123+
/**
124+
* Override setId to avoid calling session_id() which doesn't work after headers sent
125+
*/
126+
public function setId($value)
127+
{
128+
$this->_sessionId = $value;
129+
}
130+
131+
/**
132+
* Override getId to avoid calling session_id() which doesn't work after headers sent
133+
*/
134+
public function getId()
135+
{
136+
if ($this->_sessionId === null) {
137+
$this->_sessionId = session_create_id('');
138+
}
139+
return $this->_sessionId;
140+
}
141+
142+
/**
143+
* Override getIsActive to check our internal state instead of PHP session state
144+
*/
145+
public function getIsActive()
146+
{
147+
return !$this->isClosed && $this->_sessionId !== null;
148+
}
109149

110150
private function registerCoroutineCloseHandler(): void
111151
{
112152
$cid = Coroutine::getCid();
153+
113154
if ($cid < 0) {
114155
return;
115156
}

0 commit comments

Comments
 (0)