11<?php namespace com \mongodb ;
22
33use com \mongodb \io \Protocol ;
4- use lang \{Closeable , IllegalStateException };
5- use util \UUID ;
4+ use lang \{Closeable , IllegalStateException , Value , Throwable };
5+ use util \{ UUID , Objects } ;
66
77/**
88 * A client session
99 *
1010 * @see https://docs.mongodb.com/manual/reference/server-sessions/
1111 * @see https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/#std-label-sessions
1212 * @see https://github.com/mongodb/specifications/blob/master/source/sessions/driver-sessions.rst
13+ * @test com.mongodb.unittest.SessionsTest
1314 */
14- class Session implements Closeable {
15+ class Session implements Value, Closeable {
1516 private $ proto , $ id ;
1617 private $ closed = false ;
18+ private $ transaction = ['n ' => 0 ];
1719
1820 /**
1921 * Creates a new session
@@ -33,31 +35,147 @@ public function id(): UUID { return $this->id; }
3335 public function closed (): bool { return $ this ->closed ; }
3436
3537 /**
36- * Returns fields to be sent along with the command
38+ * Starts a multi-document transaction associated with the session. At any
39+ * given time, you can have at most one open transaction for a session.
40+ *
41+ * @see https://github.com/mongodb/specifications/blob/master/source/transactions/transactions.rst#transactionoptions
42+ * @param ?string $options
43+ * @return self
44+ * @throws lang.IllegalStateException if a transaction has already been started
45+ */
46+ public function transaction ($ options = null ): self {
47+ if (isset ($ this ->transaction ['context ' ])) {
48+ throw new IllegalStateException ('Cannot start more than one transaction on a session ' );
49+ }
50+
51+ null === $ options ? $ params = [] : parse_str ($ options , $ params );
52+ $ this ->transaction ['context ' ]= [
53+ 'txnNumber ' => new Int64 (++$ this ->transaction ['n ' ]),
54+ 'autocommit ' => false ,
55+ 'startTransaction ' => true ,
56+ ];
57+
58+ // Overwrite readPreference, defaults to inheriting from client
59+ isset ($ params ['readPreference ' ]) && $ this ->transaction ['context ' ]['$readPreference ' ]= ['mode ' => $ params ['readPreference ' ]];
60+
61+ // Support timeoutMS and the deprecated maxCommitTimeMS
62+ $ timeout = $ params ['timeoutMS ' ] ?? $ params ['maxCommitTimeMS ' ] ?? null ;
63+ $ this ->transaction ['t ' ]= null === $ timeout ? [] : ['maxTimeMS ' => new Int64 ($ timeout )];
64+
65+ return $ this ;
66+ }
67+
68+ /**
69+ * Commits a transaction
70+ *
71+ * @return void
72+ * @throws lang.IllegalStateException if no transaction is active
73+ * @throws com.mongodb.Error
74+ */
75+ public function commit () {
76+ if (!isset ($ this ->transaction ['context ' ])) {
77+ throw new IllegalStateException ('No active transaction ' );
78+ }
79+
80+ try {
81+ if (!isset ($ this ->transaction ['context ' ]['startTransaction ' ])) {
82+ $ this ->proto ->write ($ this , ['commitTransaction ' => 1 , '$db ' => 'admin ' ] + $ this ->transaction ['t ' ] + $ this ->transaction ['context ' ]);
83+ }
84+ } finally {
85+ unset($ this ->transaction ['context ' ]);
86+ }
87+ }
88+
89+ /**
90+ * Aborts a transaction
91+ *
92+ * @return void
93+ * @throws lang.IllegalStateException if no transaction is active
94+ * @throws com.mongodb.Error
95+ */
96+ public function abort () {
97+ if (!isset ($ this ->transaction ['context ' ])) {
98+ throw new IllegalStateException ('No active transaction ' );
99+ }
100+
101+ try {
102+ if (!isset ($ this ->transaction ['context ' ]['startTransaction ' ])) {
103+ $ this ->proto ->write ($ this , ['abortTransaction ' => 1 , '$db ' => 'admin ' ] + $ this ->transaction ['context ' ]);
104+ }
105+ } finally {
106+ unset($ this ->transaction ['context ' ]);
107+ }
108+ }
109+
110+ /**
111+ * Returns fields to be sent along with the command. Used by Protocol class.
37112 *
38113 * @param com.mongodb.io.Protocol
39114 * @return [:var]
40115 * @throws lang.IllegalStateException
41116 */
42117 public function send ($ proto ) {
43- if ($ proto === $ this ->proto ) return ['lsid ' => ['id ' => $ this ->id ]];
118+ if ($ proto !== $ this ->proto ) {
119+ throw new IllegalStateException ('Session was created by a different client ' );
120+ }
44121
45- throw new IllegalStateException ('Session was created by a different client ' );
122+ // When constructing the first command within a transaction, drivers MUST
123+ // add the lsid, txnNumber, startTransaction, and autocommit fields. When
124+ // constructing any other command within a transaction, drivers MUST add
125+ // the lsid, txnNumber, and autocommit fields.
126+ $ fields = ['lsid ' => ['id ' => $ this ->id ]];
127+ if (isset ($ this ->transaction ['context ' ])) {
128+ $ fields += $ this ->transaction ['context ' ];
129+ unset($ this ->transaction ['context ' ]['startTransaction ' ]);
130+ }
131+ return $ fields ;
46132 }
47133
48134 /** @return void */
49135 public function close () {
50136 if ($ this ->closed ) return ;
51137
138+ // Should there be an active running transaction, abort it.
139+ if (isset ($ this ->transaction ['context ' ])) {
140+ try {
141+ $ this ->proto ->write ($ this , ['abortTransaction ' => 1 , '$db ' => 'admin ' ] + $ this ->transaction ['context ' ]);
142+ } catch (Throwable $ ignored ) {
143+ // NOOP
144+ }
145+ unset($ this ->transaction ['context ' ]);
146+ }
147+
52148 // Fire and forget: If the user has no session that match, the endSessions call has
53149 // no effect, see https://docs.mongodb.com/manual/reference/command/endSessions/
54- $ this ->proto ->read ($ this , [
55- 'endSessions ' => [['id ' => $ this ->id ]],
56- '$db ' => 'admin '
57- ]);
150+ try {
151+ $ this ->proto ->write ($ this , ['endSessions ' => [['id ' => $ this ->id ]], '$db ' => 'admin ' ]);
152+ } catch (Throwable $ ignored ) {
153+ // NOOP
154+ }
58155 $ this ->closed = true ;
59156 }
60157
158+ /** @return string */
159+ public function hashCode () {
160+ return 'S ' .$ this ->id ->hashCode ();
161+ }
162+
163+ /** @return string */
164+ public function toString () {
165+ $ context = isset ($ this ->transaction ['context ' ]) ? ', transaction: ' .Objects::stringOf ($ this ->transaction ['context ' ]) : '' ;
166+ return nameof ($ this ).'(id: ' .$ this ->id ->hashCode ().$ context .') ' ;
167+ }
168+
169+ /**
170+ * Comparison
171+ *
172+ * @param var $value
173+ * @return int
174+ */
175+ public function compareTo ($ value ) {
176+ return $ value instanceof self ? $ this ->id ->compareTo ($ value ->id ) : 1 ;
177+ }
178+
61179 /** @return void */
62180 public function __destruct () {
63181 $ this ->close ();
0 commit comments