Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like PdoSessionHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use PdoSessionHandler, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
41 | class PdoSessionHandler implements \SessionHandlerInterface |
||
42 | { |
||
43 | /** |
||
44 | * No locking is done. This means sessions are prone to loss of data due to |
||
45 | * race conditions of concurrent requests to the same session. The last session |
||
46 | * write will win in this case. It might be useful when you implement your own |
||
47 | * logic to deal with this like an optimistic approach. |
||
48 | */ |
||
49 | const LOCK_NONE = 0; |
||
50 | |||
51 | /** |
||
52 | * Creates an application-level lock on a session. The disadvantage is that the |
||
53 | * lock is not enforced by the database and thus other, unaware parts of the |
||
54 | * application could still concurrently modify the session. The advantage is it |
||
55 | * does not require a transaction. |
||
56 | * This mode is not available for SQLite and not yet implemented for oci and sqlsrv. |
||
57 | */ |
||
58 | const LOCK_ADVISORY = 1; |
||
59 | |||
60 | /** |
||
61 | * Issues a real row lock. Since it uses a transaction between opening and |
||
62 | * closing a session, you have to be careful when you use same database connection |
||
63 | * that you also use for your application logic. This mode is the default because |
||
64 | * it's the only reliable solution across DBMSs. |
||
65 | */ |
||
66 | const LOCK_TRANSACTIONAL = 2; |
||
67 | |||
68 | /** |
||
69 | * @var \PDO|null PDO instance or null when not connected yet |
||
70 | */ |
||
71 | private $pdo; |
||
72 | |||
73 | /** |
||
74 | * @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled |
||
75 | */ |
||
76 | private $dsn = false; |
||
77 | |||
78 | /** |
||
79 | * @var string Database driver |
||
80 | */ |
||
81 | private $driver; |
||
82 | |||
83 | /** |
||
84 | * @var string Table name |
||
85 | */ |
||
86 | private $table = 'sessions'; |
||
87 | |||
88 | /** |
||
89 | * @var string Column for session id |
||
90 | */ |
||
91 | private $idCol = 'sess_id'; |
||
92 | |||
93 | /** |
||
94 | * @var string Column for session data |
||
95 | */ |
||
96 | private $dataCol = 'sess_data'; |
||
97 | |||
98 | /** |
||
99 | * @var string Column for lifetime |
||
100 | */ |
||
101 | private $lifetimeCol = 'sess_lifetime'; |
||
102 | |||
103 | /** |
||
104 | * @var string Column for timestamp |
||
105 | */ |
||
106 | private $timeCol = 'sess_time'; |
||
107 | |||
108 | /** |
||
109 | * @var string Username when lazy-connect |
||
110 | */ |
||
111 | private $username = ''; |
||
112 | |||
113 | /** |
||
114 | * @var string Password when lazy-connect |
||
115 | */ |
||
116 | private $password = ''; |
||
117 | |||
118 | /** |
||
119 | * @var array Connection options when lazy-connect |
||
120 | */ |
||
121 | private $connectionOptions = array(); |
||
122 | |||
123 | /** |
||
124 | * @var int The strategy for locking, see constants |
||
125 | */ |
||
126 | private $lockMode = self::LOCK_TRANSACTIONAL; |
||
127 | |||
128 | /** |
||
129 | * It's an array to support multiple reads before closing which is manual, non-standard usage. |
||
130 | * |
||
131 | * @var \PDOStatement[] An array of statements to release advisory locks |
||
132 | */ |
||
133 | private $unlockStatements = array(); |
||
134 | |||
135 | /** |
||
136 | * @var bool True when the current session exists but expired according to session.gc_maxlifetime |
||
137 | */ |
||
138 | private $sessionExpired = false; |
||
139 | |||
140 | /** |
||
141 | * @var bool Whether a transaction is active |
||
142 | */ |
||
143 | private $inTransaction = false; |
||
144 | |||
145 | /** |
||
146 | * @var bool Whether gc() has been called |
||
147 | */ |
||
148 | private $gcCalled = false; |
||
149 | |||
150 | /** |
||
151 | * Constructor. |
||
152 | * |
||
153 | * You can either pass an existing database connection as PDO instance or |
||
154 | * pass a DSN string that will be used to lazy-connect to the database |
||
155 | * when the session is actually used. Furthermore it's possible to pass null |
||
156 | * which will then use the session.save_path ini setting as PDO DSN parameter. |
||
157 | * |
||
158 | * List of available options: |
||
159 | * * db_table: The name of the table [default: sessions] |
||
160 | * * db_id_col: The column where to store the session id [default: sess_id] |
||
161 | * * db_data_col: The column where to store the session data [default: sess_data] |
||
162 | * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime] |
||
163 | * * db_time_col: The column where to store the timestamp [default: sess_time] |
||
164 | * * db_username: The username when lazy-connect [default: ''] |
||
165 | * * db_password: The password when lazy-connect [default: ''] |
||
166 | * * db_connection_options: An array of driver-specific connection options [default: array()] |
||
167 | * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] |
||
168 | * |
||
169 | * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null |
||
170 | * @param array $options An associative array of options |
||
171 | * |
||
172 | * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION |
||
173 | */ |
||
174 | public function __construct($pdoOrDsn = null, array $options = array()) |
||
197 | |||
198 | /** |
||
199 | * Creates the table to store sessions which can be called once for setup. |
||
200 | * |
||
201 | * Session ID is saved in a column of maximum length 128 because that is enough even |
||
202 | * for a 512 bit configured session.hash_function like Whirlpool. Session data is |
||
203 | * saved in a BLOB. One could also use a shorter inlined varbinary column |
||
204 | * if one was sure the data fits into it. |
||
205 | * |
||
206 | * @throws \PDOException When the table already exists |
||
207 | * @throws \DomainException When an unsupported PDO driver is used |
||
208 | */ |
||
209 | public function createTable() |
||
247 | |||
248 | /** |
||
249 | * Returns true when the current session exists but expired according to session.gc_maxlifetime. |
||
250 | * |
||
251 | * Can be used to distinguish between a new session and one that expired due to inactivity. |
||
252 | * |
||
253 | * @return bool Whether current session expired |
||
254 | */ |
||
255 | public function isSessionExpired() |
||
259 | |||
260 | /** |
||
261 | * {@inheritdoc} |
||
262 | */ |
||
263 | public function open($savePath, $sessionName) |
||
271 | |||
272 | /** |
||
273 | * {@inheritdoc} |
||
274 | */ |
||
275 | public function read($sessionId) |
||
285 | |||
286 | /** |
||
287 | * {@inheritdoc} |
||
288 | */ |
||
289 | public function gc($maxlifetime) |
||
297 | |||
298 | /** |
||
299 | * {@inheritdoc} |
||
300 | */ |
||
301 | public function destroy($sessionId) |
||
318 | |||
319 | /** |
||
320 | * {@inheritdoc} |
||
321 | */ |
||
322 | public function write($sessionId, $data) |
||
376 | |||
377 | /** |
||
378 | * {@inheritdoc} |
||
379 | */ |
||
380 | public function close() |
||
405 | |||
406 | /** |
||
407 | * Lazy-connects to the database. |
||
408 | * |
||
409 | * @param string $dsn DSN string |
||
410 | */ |
||
411 | private function connect($dsn) |
||
417 | |||
418 | /** |
||
419 | * Helper method to begin a transaction. |
||
420 | * |
||
421 | * Since SQLite does not support row level locks, we have to acquire a reserved lock |
||
422 | * on the database immediately. Because of https://bugs.php.net/42766 we have to create |
||
423 | * such a transaction manually which also means we cannot use PDO::commit or |
||
424 | * PDO::rollback or PDO::inTransaction for SQLite. |
||
425 | * |
||
426 | * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions |
||
427 | * due to http://www.mysqlperformanceblog.com/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . |
||
428 | * So we change it to READ COMMITTED. |
||
429 | */ |
||
430 | private function beginTransaction() |
||
444 | |||
445 | /** |
||
446 | * Helper method to commit a transaction. |
||
447 | */ |
||
448 | View Code Duplication | private function commit() |
|
466 | |||
467 | /** |
||
468 | * Helper method to rollback a transaction. |
||
469 | */ |
||
470 | View Code Duplication | private function rollback() |
|
485 | |||
486 | /** |
||
487 | * Reads the session data in respect to the different locking strategies. |
||
488 | * |
||
489 | * We need to make sure we do not return session data that is already considered garbage according |
||
490 | * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. |
||
491 | * |
||
492 | * @param string $sessionId Session ID |
||
493 | * |
||
494 | * @return string The session data |
||
495 | */ |
||
496 | private function doRead($sessionId) |
||
553 | |||
554 | /** |
||
555 | * Executes an application-level lock on the database. |
||
556 | * |
||
557 | * @param string $sessionId Session ID |
||
558 | * |
||
559 | * @return \PDOStatement The statement that needs to be executed later to release the lock |
||
560 | * |
||
561 | * @throws \DomainException When an unsupported PDO driver is used |
||
562 | * |
||
563 | * @todo implement missing advisory locks |
||
564 | * - for oci using DBMS_LOCK.REQUEST |
||
565 | * - for sqlsrv using sp_getapplock with LockOwner = Session |
||
566 | */ |
||
567 | private function doAdvisoryLock($sessionId) |
||
615 | |||
616 | /** |
||
617 | * Return a locking or nonlocking SQL query to read session information. |
||
618 | * |
||
619 | * @return string The SQL string |
||
620 | * |
||
621 | * @throws \DomainException When an unsupported PDO driver is used |
||
622 | */ |
||
623 | private function getSelectSql() |
||
645 | |||
646 | /** |
||
647 | * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. |
||
648 | * |
||
649 | * @param string $sessionId Session ID |
||
650 | * @param string $data Encoded session data |
||
651 | * @param int $maxlifetime session.gc_maxlifetime |
||
652 | * |
||
653 | * @return \PDOStatement|null The merge statement or null when not supported |
||
654 | */ |
||
655 | private function getMergeStatement($sessionId, $data, $maxlifetime) |
||
656 | { |
||
657 | $mergeSql = null; |
||
658 | switch (true) { |
||
659 | case 'mysql' === $this->driver: |
||
660 | $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". |
||
661 | "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; |
||
662 | break; |
||
663 | case 'oci' === $this->driver: |
||
664 | // DUAL is Oracle specific dummy table |
||
665 | $mergeSql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". |
||
666 | "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". |
||
667 | "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; |
||
668 | break; |
||
669 | case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): |
||
670 | // MERGE is only available since SQL Server 2008 and must be terminated by semicolon |
||
671 | // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx |
||
672 | $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". |
||
673 | "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". |
||
674 | "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; |
||
675 | break; |
||
676 | View Code Duplication | case 'sqlite' === $this->driver: |
|
677 | $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; |
||
678 | break; |
||
679 | case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): |
||
680 | $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". |
||
681 | "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; |
||
682 | break; |
||
683 | } |
||
684 | |||
685 | if (null !== $mergeSql) { |
||
686 | $mergeStmt = $this->pdo->prepare($mergeSql); |
||
687 | |||
688 | if ('sqlsrv' === $this->driver || 'oci' === $this->driver) { |
||
689 | $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); |
||
690 | $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); |
||
691 | $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); |
||
692 | $mergeStmt->bindParam(4, $maxlifetime, \PDO::PARAM_INT); |
||
693 | $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); |
||
694 | $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); |
||
695 | $mergeStmt->bindParam(7, $maxlifetime, \PDO::PARAM_INT); |
||
696 | $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); |
||
697 | } else { |
||
698 | $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); |
||
699 | $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); |
||
700 | $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); |
||
701 | $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); |
||
702 | } |
||
703 | |||
704 | return $mergeStmt; |
||
705 | } |
||
706 | } |
||
707 | |||
708 | /** |
||
709 | * Return a PDO instance. |
||
710 | * |
||
711 | * @return \PDO |
||
712 | */ |
||
713 | protected function getConnection() |
||
721 | } |
||
722 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.