diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt new file mode 100644 index 0000000000000..dfef3bb4839f2 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_basic.phpt @@ -0,0 +1,132 @@ +--TEST-- +PDO Common: Set PDOStatement class with PDO::ATTR_STATEMENT_CLASS overall test +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + +$table = 'pdo_attr_statement_class_basic'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); + +$default = $db->getAttribute(PDO::ATTR_STATEMENT_CLASS); +var_dump($default); + +// Having a public destructor is allowed +class StatementWithPublicDestructor extends PDOStatement { + public function __destruct() { + echo __METHOD__, PHP_EOL; + } +} + +try { + var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['StatementWithPublicDestructor', []])); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} +var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['StatementWithPublicDestructor'])); +$stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +unset($stmt); + +echo "Class derived from PDOStatement, with private constructor:\n"; +class StatementWithPrivateConstructor extends PDOStatement { + private function __construct($msg) { + echo __METHOD__, PHP_EOL; + var_dump($this); + var_dump($msg); + } +} +var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['StatementWithPrivateConstructor', ['param1']])); +$stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +unset($stmt); + +echo "Class derived from a child of PDOStatement:\n"; +class StatementDerivedFromChild extends StatementWithPrivateConstructor { + public function fetchAll($fetch_style = 1, ...$fetch_args): array { + return []; + } +} + +var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['StatementDerivedFromChild', ['param1']])); +$stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +var_dump($stmt->fetchAll()); +unset($stmt); + +echo "Reset to default PDOStatement class:\n"; +var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['PDOStatement'])); +$stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +var_dump($stmt->fetchAll()); +unset($stmt); + +?> +--CLEAN-- + +--EXPECT-- +array(1) { + [0]=> + string(12) "PDOStatement" +} +bool(true) +bool(true) +StatementWithPublicDestructor::__destruct +Class derived from PDOStatement, with private constructor: +bool(true) +StatementWithPrivateConstructor::__construct +object(StatementWithPrivateConstructor)#2 (1) { + ["queryString"]=> + string(68) "SELECT id, label FROM pdo_attr_statement_class_basic ORDER BY id ASC" +} +string(6) "param1" +Class derived from a child of PDOStatement: +bool(true) +StatementWithPrivateConstructor::__construct +object(StatementDerivedFromChild)#2 (1) { + ["queryString"]=> + string(68) "SELECT id, label FROM pdo_attr_statement_class_basic ORDER BY id ASC" +} +string(6) "param1" +array(0) { +} +Reset to default PDOStatement class: +bool(true) +array(2) { + [0]=> + array(4) { + ["id"]=> + string(1) "1" + [0]=> + string(1) "1" + ["label"]=> + string(1) "a" + [1]=> + string(1) "a" + } + [1]=> + array(4) { + ["id"]=> + string(1) "2" + [0]=> + string(1) "2" + ["label"]=> + string(1) "b" + [1]=> + string(1) "b" + } +} diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt new file mode 100644 index 0000000000000..301a8835eee2e --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt @@ -0,0 +1,58 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed with GC intervention +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, [$this->statementClass, [$this]]); + } +} + +$db = PDOTest::factory(Bar::class); + +$table = 'pdo_attr_statement_class_ctor_arg_gc'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; +$stmt = $db->query($query); + +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); + +?> +--CLEAN-- + +--EXPECT-- +object(Bar)#1 (1) { + ["statementClass"]=> + string(3) "Foo" +} +bool(true) +bool(true) diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_modified.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_modified.phpt new file mode 100644 index 0000000000000..bcf07b1ea8d73 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_modified.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed without GC intervention as a variable that is modified +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, $a); +$a[0] = 'Bar'; + +$table = 'pdo_attr_statement_class_ctor_arg_no_gc_var_modified'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; +$stmt = $db->query($query); + +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); + +?> +--CLEAN-- + +--EXPECT-- +string(6) "param1" +bool(true) +bool(true) diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_unset.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_unset.phpt new file mode 100644 index 0000000000000..e5d214fea5759 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no-gc_var_unset.phpt @@ -0,0 +1,49 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed without GC intervention as a variable that is unset +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, $a); +unset($a); + +$table = 'pdo_attr_statement_class_ctor_arg_no_gc_var_unset'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; +$stmt = $db->query($query); + +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); + +?> +--CLEAN-- + +--EXPECT-- +string(6) "param1" +bool(true) +bool(true) diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no_gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no_gc.phpt new file mode 100644 index 0000000000000..2a111a9ac1947 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_ctor_arg_no_gc.phpt @@ -0,0 +1,47 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed without GC intervention +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Foo', ['param1'])); + +$table = 'pdo_attr_statement_class_ctor_arg_no_gc'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; +$stmt = $db->query($query); + +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); + +?> +--CLEAN-- + +--EXPECT-- +string(6) "param1" +bool(true) +bool(true) diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt new file mode 100644 index 0000000000000..61cfe56f779d9 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_cyclic_ctor_args.phpt @@ -0,0 +1,59 @@ +--TEST-- +PDO Common: Set PDOStatement class with PDO::ATTR_STATEMENT_CLASS overall test +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + +$table = 'pdo_attr_statement_class_cyclic_ctor_args'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); + +$default = $db->getAttribute(PDO::ATTR_STATEMENT_CLASS); +var_dump($default); + +class HoldPdo extends PDOStatement { + private function __construct(public PDO $v) { + var_dump($v); + } +} + +var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['HoldPdo', [$db]])); +$stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +var_dump($stmt); + +?> +--CLEAN-- + +--EXPECT-- +array(1) { + [0]=> + string(12) "PDOStatement" +} +bool(true) +object(PDO)#1 (0) { +} +object(HoldPdo)#2 (2) { + ["queryString"]=> + string(79) "SELECT id, label FROM pdo_attr_statement_class_cyclic_ctor_args ORDER BY id ASC" + ["v"]=> + object(PDO)#1 (0) { + } +} diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_errors.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_errors.phpt new file mode 100644 index 0000000000000..c65116d3e5088 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_errors.phpt @@ -0,0 +1,79 @@ +--TEST-- +PDO Common: Set PDOStatement class with PDO::ATTR_STATEMENT_CLASS various error conditions +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, 'not an array'); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} + +echo "Unknown class variation:\n"; +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['classname']); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} + +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['classname', []]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} + +echo "Class not derived from PDOStatement:\n"; +class NotDerived { +} + +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['NotDerived']); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['NotDerived', []]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} + +echo "Class derived from PDOStatement, but with public constructor:\n"; +class DerivedWithPublicConstructor extends PDOStatement { + public function __construct() { } +} + +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['DerivedWithPublicConstructor']); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} +try { + $db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['DerivedWithPublicConstructor', []]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} +?> +--EXPECT-- +TypeError: PDO::setAttribute(): Argument #2 ($value) PDO::ATTR_STATEMENT_CLASS value must be of type array, string given +Unknown class variation: +TypeError: PDO::setAttribute(): Argument #2 ($value) PDO::ATTR_STATEMENT_CLASS class must be a valid class +TypeError: PDO::setAttribute(): Argument #2 ($value) PDO::ATTR_STATEMENT_CLASS class must be a valid class +Class not derived from PDOStatement: +TypeError: PDO::setAttribute(): Argument #2 ($value) PDO::ATTR_STATEMENT_CLASS class must be derived from PDOStatement +TypeError: PDO::setAttribute(): Argument #2 ($value) PDO::ATTR_STATEMENT_CLASS class must be derived from PDOStatement +Class derived from PDOStatement, but with public constructor: +TypeError: PDO::setAttribute(): Argument #2 ($value) User-supplied statement class cannot have a public constructor +TypeError: PDO::setAttribute(): Argument #2 ($value) User-supplied statement class cannot have a public constructor diff --git a/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_with_abstract_class.phpt b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_with_abstract_class.phpt new file mode 100644 index 0000000000000..339f4ec0be58d --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_ATTR_STATEMENT_CLASS_with_abstract_class.phpt @@ -0,0 +1,42 @@ +--TEST-- +PDO Common: Set PDOStatement class with PDO::ATTR_STATEMENT_CLASS that is abstract +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); + +abstract class DerivedButAbstract extends PDOStatement { + private function __construct() {} +} + +try { + var_dump($db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['DerivedButAbstract', ['DerivedButAbstract']])); + $stmt = $db->query("SELECT id, label FROM {$table} ORDER BY id ASC"); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), \PHP_EOL; +} +?> +--CLEAN-- + +--EXPECT-- +bool(true) +Error: Cannot instantiate abstract class DerivedButAbstract diff --git a/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt new file mode 100644 index 0000000000000..86d2cfcefd5fd --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_gc.phpt @@ -0,0 +1,61 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed with GC intervention in PDO::prepare() +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, [$this->statementClass, [$this]]); + } +} + +$db = PDOTest::factory(Bar::class); + +$table = 'pdo_prepare_attr_statement_class_ctor_arg_gc'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); + +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; + +$stmt = $db->prepare($query); +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); +$r = $stmt->execute(); +var_dump($r); +?> +--CLEAN-- + +--EXPECT-- +object(Bar)#1 (1) { + ["statementClass"]=> + string(3) "Foo" +} +bool(true) +bool(true) +bool(true) diff --git a/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_no-gc.phpt b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_no-gc.phpt new file mode 100644 index 0000000000000..bf406b8952750 --- /dev/null +++ b/ext/pdo/tests/attr_statement_class/pdo_prepare_ATTR_STATEMENT_CLASS_ctor_arg_no-gc.phpt @@ -0,0 +1,51 @@ +--TEST-- +PDO Common: Set PDOStatement class with ctor_args that are freed without GC intervention in PDO::prepare() +--EXTENSIONS-- +pdo +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_STATEMENT_CLASS, ['Foo', ['param1']]); + +$table = 'pdo_prepare_attr_statement_class_ctor_arg_no_gc'; +$db->exec("CREATE TABLE {$table} (id INT, label CHAR(1), PRIMARY KEY(id))"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (1, 'a')"); +$db->exec("INSERT INTO {$table} (id, label) VALUES (2, 'b')"); + +$query = "SELECT id, label FROM {$table} ORDER BY id ASC"; + +//$stmt = $db->prepare($dummy_query, [PDO::ATTR_STATEMENT_CLASS => ['Foo', ['param1']] ]); +$stmt = $db->prepare($query); + +var_dump($stmt instanceof Foo); +var_dump($stmt->queryString === $query); +$r = $stmt->execute(); +var_dump($r); +?> +--CLEAN-- + +--EXPECT-- +string(6) "param1" +bool(true) +bool(true) +bool(true) diff --git a/ext/pdo/tests/pdo_test.inc b/ext/pdo/tests/pdo_test.inc index 8cd60f68aa519..b44d0b88e77b7 100644 --- a/ext/pdo/tests/pdo_test.inc +++ b/ext/pdo/tests/pdo_test.inc @@ -96,5 +96,20 @@ class PDOTest { }; } } +/** See https://stackoverflow.com/a/3732466 */ +function get_dummy_sql_request(): string +{ + $dsn = getenv('PDOTEST_DSN'); -?> + // Firebird: https://www.firebirdfaq.org/faq30/ + if (str_starts_with($dsn, 'firebird')) { + return "SELECT FIRST 0 FROM (VALUES ('Hello world')"; + } + + // Oracle + if (str_starts_with($dsn, 'oci')) { + return 'SELECT 1 FROM DUAL'; + } + + return 'SELECT 1'; +}