Skip to content

Commit 2257bf1

Browse files
committed
PDO - Integrate regular resource with persistent connection for end-of-request hook to invoke persistent shutdown and rollback.
Rework connectivity checks to generate SQLSTATE error 01002 according to error mode, rather than always throw error.
1 parent 28afd52 commit 2257bf1

File tree

6 files changed

+123
-33
lines changed

6 files changed

+123
-33
lines changed

ext/pdo/pdo.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ PHP_MINIT_FUNCTION(pdo)
247247

248248
zend_hash_init(&pdo_driver_hash, 0, NULL, NULL, 1);
249249

250-
le_ppdo = zend_register_list_destructors_ex(NULL, php_pdo_pdbh_dtor,
250+
le_ppdo = zend_register_list_destructors_ex(php_pdo_pdbh_request_dtor, php_pdo_pdbh_dtor,
251251
"PDO persistent database", module_number);
252252

253253
pdo_exception_ce = register_class_PDOException(spl_ce_RuntimeException);

ext/pdo/pdo_dbh.c

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ PHP_METHOD(PDO, __construct)
313313
int plen = 0;
314314
char *hashkey = NULL;
315315
pdo_dbh_t *pdbh = NULL;
316+
pdo_dbh_t **pdbh_ref = NULL;
316317
zval *v;
317318

318319
if ((v = zend_hash_index_find_deref(Z_ARRVAL_P(options), PDO_ATTR_PERSISTENT)) != NULL) {
@@ -338,9 +339,22 @@ PHP_METHOD(PDO, __construct)
338339
/* is the connection still alive ? */
339340
if (!pdbh || pdbh->is_closed ||
340341
(pdbh->methods->check_liveness && FAILURE == (pdbh->methods->check_liveness)(pdbh))) {
342+
/* clean up prior dbh reference */
343+
if (pdbh && pdbh->persistent_resource) {
344+
pdbh_ref = (pdo_dbh_t**)pdbh->persistent_resource->ptr;
345+
/* clear dbh reference to forestall end-of-request actions in destructor */
346+
*pdbh_ref = NULL;
347+
zend_list_delete(pdbh->persistent_resource);
348+
pdbh->persistent_resource = NULL;
349+
}
341350
/* need a brand new pdbh */
342351
pdbh = pecalloc(1, sizeof(*pdbh), 1);
343-
352+
pdbh_ref = emalloc(sizeof(*pdbh_ref));
353+
*pdbh_ref = pdbh;
354+
pdbh->persistent_resource = zend_register_resource(pdbh_ref, php_pdo_list_entry());
355+
if (!pdbh->persistent_resource) {
356+
php_error_docref(NULL, E_ERROR, "Failed to register resource entry");
357+
}
344358
pdbh->is_persistent = 1;
345359
pdbh->persistent_id = pemalloc(plen + 1, 1);
346360
memcpy((char *)pdbh->persistent_id, hashkey, plen+1);
@@ -394,7 +408,7 @@ PHP_METHOD(PDO, __construct)
394408
/* we should also need to replace the object store entry,
395409
since it was created with emalloc */
396410
/* if a resource is already registered, then it failed a liveness check
397-
* and will be replaced, prompting destruct. */
411+
and will be replaced, prompting destruct. */
398412
if ((zend_register_persistent_resource(
399413
(char*)dbh->persistent_id, dbh->persistent_id_len, dbh, php_pdo_list_entry())) == NULL) {
400414
php_error_docref(NULL, E_ERROR, "Failed to register persistent entry");
@@ -517,6 +531,8 @@ PHP_METHOD(PDO, prepare)
517531

518532
PDO_DBH_CLEAR_ERR();
519533

534+
PDO_CLOSE_CHECK;
535+
520536
if (options && (value = zend_hash_index_find(Z_ARRVAL_P(options), PDO_ATTR_STATEMENT_CLASS)) != NULL) {
521537
if (Z_TYPE_P(value) != IS_ARRAY) {
522538
zend_type_error("PDO::ATTR_STATEMENT_CLASS value must be of type array, %s given",
@@ -604,6 +620,8 @@ PHP_METHOD(PDO, beginTransaction)
604620

605621
PDO_CONSTRUCT_CHECK;
606622

623+
PDO_CLOSE_CHECK;
624+
607625
if (pdo_is_in_transaction(dbh)) {
608626
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is already an active transaction");
609627
RETURN_THROWS();
@@ -634,6 +652,8 @@ PHP_METHOD(PDO, commit)
634652

635653
PDO_CONSTRUCT_CHECK;
636654

655+
PDO_CLOSE_CHECK;
656+
637657
if (!pdo_is_in_transaction(dbh)) {
638658
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
639659
RETURN_THROWS();
@@ -658,6 +678,8 @@ PHP_METHOD(PDO, rollBack)
658678

659679
PDO_CONSTRUCT_CHECK;
660680

681+
PDO_CLOSE_CHECK;
682+
661683
if (!pdo_is_in_transaction(dbh)) {
662684
zend_throw_exception_ex(php_pdo_get_exception(), 0, "There is no active transaction");
663685
RETURN_THROWS();
@@ -682,6 +704,8 @@ PHP_METHOD(PDO, inTransaction)
682704

683705
PDO_CONSTRUCT_CHECK;
684706

707+
PDO_CLOSE_CHECK;
708+
685709
RETURN_BOOL(pdo_is_in_transaction(dbh));
686710
}
687711
/* }}} */
@@ -693,6 +717,8 @@ PHP_METHOD(PDO, isConnected)
693717

694718
ZEND_PARSE_PARAMETERS_NONE();
695719

720+
PDO_CONSTRUCT_CHECK;
721+
696722
RETURN_BOOL(!dbh->is_closed);
697723
}
698724
/* }}} */
@@ -900,6 +926,7 @@ PHP_METHOD(PDO, setAttribute)
900926

901927
PDO_DBH_CLEAR_ERR();
902928
PDO_CONSTRUCT_CHECK;
929+
PDO_CLOSE_CHECK;
903930

904931
RETURN_BOOL(pdo_dbh_attribute_set(dbh, attr, value));
905932
}
@@ -954,6 +981,12 @@ PHP_METHOD(PDO, getAttribute)
954981
break;
955982
}
956983

984+
if (!dbh->methods) {
985+
pdo_raise_impl_error(dbh, NULL, "IM001",
986+
"driver attributes not initialized, possibly due to disconnect");
987+
RETURN_FALSE;
988+
}
989+
957990
if (!dbh->methods->get_attribute) {
958991
pdo_raise_impl_error(dbh, NULL, "IM001", "driver does not support getting attributes");
959992
RETURN_FALSE;
@@ -994,6 +1027,8 @@ PHP_METHOD(PDO, exec)
9941027

9951028
PDO_DBH_CLEAR_ERR();
9961029
PDO_CONSTRUCT_CHECK;
1030+
PDO_CLOSE_CHECK;
1031+
9971032
ret = dbh->methods->doer(dbh, statement);
9981033
if (ret == -1) {
9991034
PDO_HANDLE_DBH_ERR();
@@ -1020,6 +1055,8 @@ PHP_METHOD(PDO, lastInsertId)
10201055

10211056
PDO_DBH_CLEAR_ERR();
10221057

1058+
PDO_CLOSE_CHECK;
1059+
10231060
if (!dbh->methods->last_id) {
10241061
pdo_raise_impl_error(dbh, NULL, "IM001", "driver does not support lastInsertId()");
10251062
RETURN_FALSE;
@@ -1040,6 +1077,8 @@ PHP_METHOD(PDO, errorCode)
10401077

10411078
ZEND_PARSE_PARAMETERS_NONE();
10421079

1080+
PDO_CONSTRUCT_CHECK;
1081+
10431082
if (dbh->query_stmt) {
10441083
RETURN_STRING(dbh->query_stmt->error_code);
10451084
}
@@ -1067,6 +1106,8 @@ PHP_METHOD(PDO, errorInfo)
10671106

10681107
ZEND_PARSE_PARAMETERS_NONE();
10691108

1109+
PDO_CONSTRUCT_CHECK;
1110+
10701111
array_init(return_value);
10711112

10721113
if (dbh->query_stmt) {
@@ -1127,6 +1168,8 @@ PHP_METHOD(PDO, query)
11271168

11281169
PDO_DBH_CLEAR_ERR();
11291170

1171+
PDO_CLOSE_CHECK;
1172+
11301173
if (!pdo_stmt_instantiate(dbh, return_value, dbh->def_stmt_ce, &dbh->def_stmt_ctor_args)) {
11311174
RETURN_THROWS();
11321175
}
@@ -1194,6 +1237,9 @@ PHP_METHOD(PDO, quote)
11941237
PDO_CONSTRUCT_CHECK;
11951238

11961239
PDO_DBH_CLEAR_ERR();
1240+
1241+
PDO_CLOSE_CHECK;
1242+
11971243
if (!dbh->methods->quoter) {
11981244
pdo_raise_impl_error(dbh, NULL, "IM001", "driver does not support quoting");
11991245
RETURN_FALSE;
@@ -1379,11 +1425,6 @@ void pdo_dbh_init(int module_number)
13791425
/* Disconnect from the database and free associated driver. */
13801426
static void dbh_shutdown(pdo_dbh_t *dbh)
13811427
{
1382-
if (dbh->driver_data && dbh->methods && dbh->methods->rollback && pdo_is_in_transaction(dbh)) {
1383-
dbh->methods->rollback(dbh);
1384-
dbh->in_txn = false;
1385-
}
1386-
13871428
if (dbh->methods) {
13881429
dbh->methods->closer(dbh);
13891430
}
@@ -1442,6 +1483,11 @@ static void dbh_free(pdo_dbh_t *dbh)
14421483
pefree((char *)dbh->persistent_id, dbh->is_persistent);
14431484
}
14441485

1486+
if (dbh->persistent_resource) {
1487+
dbh->persistent_resource->ptr = NULL;
1488+
dbh->persistent_resource = NULL;
1489+
}
1490+
14451491
if (!Z_ISUNDEF(dbh->def_stmt_ctor_args)) {
14461492
zval_ptr_dtor(&dbh->def_stmt_ctor_args);
14471493
}
@@ -1475,10 +1521,6 @@ static void pdo_dbh_free_storage(zend_object *std)
14751521

14761522
/* dbh might be null if we OOMed during object initialization. */
14771523
if (dbh) {
1478-
if (dbh->is_persistent && dbh->methods && dbh->methods->persistent_shutdown) {
1479-
dbh->methods->persistent_shutdown(dbh);
1480-
}
1481-
14821524
/* stmt is not persistent, even if dbh is, so it must be freed with pdo.
14831525
* Consider copying stmt error code to dbh at this point, seemingly the reason
14841526
* that the stmt is even being held, or even better, to do that at the time of
@@ -1488,12 +1530,14 @@ static void pdo_dbh_free_storage(zend_object *std)
14881530
dbh->query_stmt = NULL;
14891531
}
14901532

1491-
/* a persisted dbh will be freed when the resource is destructed. */
1492-
if (!(--dbh->refcount) && !pdo_is_persisted(dbh)) {
1493-
if (!dbh->is_closed) {
1494-
dbh_shutdown(dbh);
1533+
if (!(--dbh->refcount)) {
1534+
/* a persisted dbh will be freed when the resource is destructed. */
1535+
if (!pdo_is_persisted(dbh)) {
1536+
if (!dbh->is_closed) {
1537+
dbh_shutdown(dbh);
1538+
}
1539+
dbh_free(dbh);
14951540
}
1496-
dbh_free(dbh);
14971541
}
14981542
}
14991543

@@ -1517,6 +1561,27 @@ zend_object *pdo_dbh_new(zend_class_entry *ce)
15171561

15181562
/* }}} */
15191563

1564+
ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_request_dtor) /* {{{ */
1565+
{
1566+
if (res->ptr) {
1567+
pdo_dbh_t **dbh_ref = (pdo_dbh_t**)res->ptr;
1568+
if (*dbh_ref) {
1569+
pdo_dbh_t *dbh = (pdo_dbh_t*)*dbh_ref;
1570+
if (dbh->methods && dbh->methods->persistent_shutdown) {
1571+
dbh->methods->persistent_shutdown(dbh);
1572+
}
1573+
if (dbh->methods && dbh->methods->rollback && pdo_is_in_transaction(dbh)) {
1574+
dbh->methods->rollback(dbh);
1575+
dbh->in_txn = false;
1576+
}
1577+
dbh->persistent_resource = NULL;
1578+
}
1579+
efree(dbh_ref);
1580+
res->ptr = NULL;
1581+
}
1582+
}
1583+
/* }}} */
1584+
15201585
ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_dtor) /* {{{ */
15211586
{
15221587
if (res->ptr) {

ext/pdo/php_pdo.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,20 @@ PHP_MINFO_FUNCTION(pdo);
5656
#define LONG_CONST(c) (zend_long) c
5757

5858
#define PDO_CONSTRUCT_CHECK \
59-
if (dbh->is_closed) { \
60-
zend_throw_exception_ex(php_pdo_get_exception(), 0, "connection is closed"); \
61-
RETURN_THROWS(); \
62-
} \
6359
if (!dbh->driver) { \
6460
zend_throw_error(NULL, "PDO object is not initialized, constructor was not called"); \
6561
RETURN_THROWS(); \
6662
} \
6763

64+
#define PDO_CLOSE_CHECK \
65+
if (dbh->is_closed) { \
66+
pdo_raise_impl_error(dbh, NULL, "01002", NULL); \
67+
if (dbh->error_mode == PDO_ERRMODE_EXCEPTION) { \
68+
RETURN_THROWS(); \
69+
} else { \
70+
RETURN_FALSE; \
71+
} \
72+
} \
73+
6874

6975
#endif /* PHP_PDO_H */

ext/pdo/php_pdo_driver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,8 @@ struct _pdo_dbh_t {
478478
/* persistent hash key associated with this handle */
479479
const char *persistent_id;
480480
size_t persistent_id_len;
481+
/* a regular resource to prompt end-of-request actions */
482+
zend_resource *persistent_resource;
481483

482484
/* counter of _pdo_dbh_object_t referencing this handle */
483485
unsigned int refcount;

ext/pdo/php_pdo_int.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ void pdo_stmt_init(void);
3131
extern zend_object *pdo_dbh_new(zend_class_entry *ce);
3232
extern const zend_function_entry pdo_dbh_functions[];
3333
extern zend_class_entry *pdo_dbh_ce;
34+
extern ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_request_dtor);
3435
extern ZEND_RSRC_DTOR_FUNC(php_pdo_pdbh_dtor);
3536

3637
extern zend_object *pdo_dbstmt_new(zend_class_entry *ce);

ext/pdo/tests/pdo_040.phpt

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,38 @@ putenv("PDOTEST_ATTR=" . serialize(array(PDO::ATTR_PERSISTENT => true)));
2929
$pdb3 = PDOTest::factory('PDO', false);
3030
var_dump($pdb3->isConnected());
3131
$pdb4 = PDOTest::factory('PDO', false);
32+
$pdb4->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
3233
var_dump($pdb4->isConnected());
3334

3435
/* disconnect regular PDO, confirm other PDOs still connected */
3536
var_dump($db1->disconnect());
3637
var_dump($db1->isConnected());
37-
try {
38-
$db1->beginTransaction();
39-
} catch (PDOException $e) {
40-
echo $e->getMessage(), \PHP_EOL;
41-
}
38+
var_dump($db1->beginTransaction());
39+
var_dump($db1->errorCode());
40+
var_dump($db1->errorInfo());
4241
var_dump($db2->isConnected());
4342
var_dump($pdb3->isConnected());
4443

4544
/* disconnect persistent PDO, confirm other persistent PDO disconnected */
4645
var_dump($pdb3->disconnect());
4746
var_dump($pdb3->isConnected());
4847
var_dump($pdb4->isConnected());
49-
try {
50-
$pdb4->disconnect();
51-
} catch (PDOException $e) {
52-
echo $e->getMessage(), \PHP_EOL;
53-
}
48+
var_dump($pdb4->disconnect());
5449

5550
/* new persistent PDO should prompt new connection */
5651
$pdb5 = PDOTest::factory('PDO', false);
5752
var_dump($pdb5->isConnected());
5853
var_dump($pdb5->inTransaction());
5954
var_dump($pdb5->beginTransaction());
6055

56+
/* new persistent connection should not be inherited */
57+
var_dump($pdb4->isConnected());
58+
try {
59+
$pdb4->beginTransaction();
60+
} catch (PDOException $e) {
61+
var_dump($e->getMessage());
62+
}
63+
6164
$db1 = null;
6265
$db2 = null; /* trigger shutdown without explicit disconnect */
6366
$pdb3 = null;
@@ -76,14 +79,27 @@ bool(true)
7679
bool(true)
7780
bool(true)
7881
bool(false)
79-
connection is closed
82+
83+
Warning: PDO::beginTransaction(): SQLSTATE[01002]: Disconnect error in %s on line %d
84+
bool(false)
85+
string(5) "01002"
86+
array(3) {
87+
[0]=>
88+
string(5) "01002"
89+
[1]=>
90+
NULL
91+
[2]=>
92+
NULL
93+
}
8094
bool(true)
8195
bool(true)
8296
bool(true)
8397
bool(false)
8498
bool(false)
85-
connection is closed
99+
bool(true)
86100
bool(true)
87101
bool(false)
88102
bool(true)
103+
bool(false)
104+
string(33) "SQLSTATE[01002]: Disconnect error"
89105
bool(true)

0 commit comments

Comments
 (0)