@@ -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