mirror of
https://github.com/php/php-src.git
synced 2024-11-27 11:53:33 +08:00
ext/pdo_pgsql: adding pgsqlSetNoticeCallback
Allows a callback to be triggered on every notice sent by PostgreSQL. Such notices can be sent with a RAISE NOTICE in PL/pgSQL; in a long running stored procedure, they prove useful as realtime checkpoint indicators. close GH-6764
This commit is contained in:
parent
182fee1447
commit
c265b9085a
2
NEWS
2
NEWS
@ -178,6 +178,8 @@ PHP NEWS
|
|||||||
. Fixed native float support with pdo_pgsql query results. (Yurunsoft)
|
. Fixed native float support with pdo_pgsql query results. (Yurunsoft)
|
||||||
. Added class PdoPgsql. (danack, kocsismate)
|
. Added class PdoPgsql. (danack, kocsismate)
|
||||||
. Retrieve the memory usage of the query result resource. (KentarouTakeda)
|
. Retrieve the memory usage of the query result resource. (KentarouTakeda)
|
||||||
|
. Added PDO::pgsqlSetNoticeCallBack method to receive DB notices.
|
||||||
|
(outtersg)
|
||||||
|
|
||||||
- PDO_SQLITE:
|
- PDO_SQLITE:
|
||||||
. Added class PdoSqlite. (danack, kocsismate)
|
. Added class PdoSqlite. (danack, kocsismate)
|
||||||
|
@ -522,6 +522,10 @@ PHP 8.4 UPGRADE NOTES
|
|||||||
. Added pcntl_getqos_class to get the QoS level (aka performance and related
|
. Added pcntl_getqos_class to get the QoS level (aka performance and related
|
||||||
energy consumption) of the current process and pcntl_setqos_class to set it.
|
energy consumption) of the current process and pcntl_setqos_class to set it.
|
||||||
|
|
||||||
|
- PDO_PGSQL:
|
||||||
|
. Added PDO::pgsqlSetNoticeCallback to allow a callback to be triggered on
|
||||||
|
every notice sent (e.g. RAISE NOTICE).
|
||||||
|
|
||||||
- PGSQL:
|
- PGSQL:
|
||||||
. Added pg_change_password to alter a given user's password. It handles
|
. Added pg_change_password to alter a given user's password. It handles
|
||||||
transparently the password encryption from the database settings.
|
transparently the password encryption from the database settings.
|
||||||
|
@ -102,9 +102,16 @@ int _pdo_pgsql_error(pdo_dbh_t *dbh, pdo_stmt_t *stmt, int errcode, const char *
|
|||||||
}
|
}
|
||||||
/* }}} */
|
/* }}} */
|
||||||
|
|
||||||
static void _pdo_pgsql_notice(pdo_dbh_t *dbh, const char *message) /* {{{ */
|
static void _pdo_pgsql_notice(void *context, const char *message) /* {{{ */
|
||||||
{
|
{
|
||||||
/* pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data; */
|
pdo_dbh_t * dbh = (pdo_dbh_t *)context;
|
||||||
|
zend_fcall_info_cache *fc = ((pdo_pgsql_db_handle *)dbh->driver_data)->notice_callback;
|
||||||
|
if (fc) {
|
||||||
|
zval zarg;
|
||||||
|
ZVAL_STRING(&zarg, message);
|
||||||
|
zend_call_known_fcc(fc, NULL, 1, &zarg, NULL);
|
||||||
|
zval_ptr_dtor_str(&zarg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* }}} */
|
/* }}} */
|
||||||
|
|
||||||
@ -125,6 +132,16 @@ static void pdo_pgsql_fetch_error_func(pdo_dbh_t *dbh, pdo_stmt_t *stmt, zval *i
|
|||||||
}
|
}
|
||||||
/* }}} */
|
/* }}} */
|
||||||
|
|
||||||
|
static void pdo_pgsql_cleanup_notice_callback(pdo_pgsql_db_handle *H) /* {{{ */
|
||||||
|
{
|
||||||
|
if (H->notice_callback) {
|
||||||
|
zend_fcc_dtor(H->notice_callback);
|
||||||
|
efree(H->notice_callback);
|
||||||
|
H->notice_callback = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* }}} */
|
||||||
|
|
||||||
/* {{{ pdo_pgsql_create_lob_stream */
|
/* {{{ pdo_pgsql_create_lob_stream */
|
||||||
static ssize_t pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
|
static ssize_t pgsql_lob_write(php_stream *stream, const char *buf, size_t count)
|
||||||
{
|
{
|
||||||
@ -229,6 +246,7 @@ static void pgsql_handle_closer(pdo_dbh_t *dbh) /* {{{ */
|
|||||||
pefree(H->lob_streams, dbh->is_persistent);
|
pefree(H->lob_streams, dbh->is_persistent);
|
||||||
H->lob_streams = NULL;
|
H->lob_streams = NULL;
|
||||||
}
|
}
|
||||||
|
pdo_pgsql_cleanup_notice_callback(H);
|
||||||
if (H->server) {
|
if (H->server) {
|
||||||
PQfinish(H->server);
|
PQfinish(H->server);
|
||||||
H->server = NULL;
|
H->server = NULL;
|
||||||
@ -1224,6 +1242,30 @@ PHP_METHOD(PDO_PGSql_Ext, pgsqlGetPid)
|
|||||||
}
|
}
|
||||||
/* }}} */
|
/* }}} */
|
||||||
|
|
||||||
|
/* {{{ proto void PDO::pgsqlSetNoticeCallback(mixed callback)
|
||||||
|
Sets a callback to receive DB notices (after client_min_messages has been set) */
|
||||||
|
PHP_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback)
|
||||||
|
{
|
||||||
|
zend_fcall_info fci = empty_fcall_info;
|
||||||
|
zend_fcall_info_cache fcc = empty_fcall_info_cache;
|
||||||
|
if (FAILURE == zend_parse_parameters(ZEND_NUM_ARGS(), "F!", &fci, &fcc)) {
|
||||||
|
RETURN_THROWS();
|
||||||
|
}
|
||||||
|
|
||||||
|
pdo_dbh_t *dbh = Z_PDO_DBH_P(ZEND_THIS);
|
||||||
|
PDO_CONSTRUCT_CHECK;
|
||||||
|
|
||||||
|
pdo_pgsql_db_handle *H = (pdo_pgsql_db_handle *)dbh->driver_data;
|
||||||
|
|
||||||
|
pdo_pgsql_cleanup_notice_callback(H);
|
||||||
|
|
||||||
|
if (ZEND_FCC_INITIALIZED(fcc)) {
|
||||||
|
H->notice_callback = emalloc(sizeof(zend_fcall_info_cache));
|
||||||
|
zend_fcc_dup(H->notice_callback, &fcc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* }}} */
|
||||||
|
|
||||||
static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
|
static const zend_function_entry *pdo_pgsql_get_driver_methods(pdo_dbh_t *dbh, int kind)
|
||||||
{
|
{
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
@ -1341,7 +1383,7 @@ static int pdo_pgsql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* {{{
|
|||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
PQsetNoticeProcessor(H->server, (void(*)(void*,const char*))_pdo_pgsql_notice, (void *)&dbh);
|
PQsetNoticeProcessor(H->server, _pdo_pgsql_notice, (void *)dbh);
|
||||||
|
|
||||||
H->attached = 1;
|
H->attached = 1;
|
||||||
H->pgoid = -1;
|
H->pgoid = -1;
|
||||||
|
@ -33,4 +33,7 @@ class PDO_PGSql_Ext {
|
|||||||
|
|
||||||
/** @tentative-return-type */
|
/** @tentative-return-type */
|
||||||
public function pgsqlGetPid(): int {}
|
public function pgsqlGetPid(): int {}
|
||||||
|
|
||||||
|
/** @tentative-return-type */
|
||||||
|
public function pgsqlSetNoticeCallback(?callable $callback): void {}
|
||||||
}
|
}
|
||||||
|
8
ext/pdo_pgsql/pgsql_driver_arginfo.h
generated
8
ext/pdo_pgsql/pgsql_driver_arginfo.h
generated
@ -1,5 +1,5 @@
|
|||||||
/* This is a generated file, edit the .stub.php file instead.
|
/* This is a generated file, edit the .stub.php file instead.
|
||||||
* Stub hash: 9bb79af98dbb7c171fd9533aeabece4937a06cd2 */
|
* Stub hash: 14174ab18f198b9916f83986d10c93b657d8ffb9 */
|
||||||
|
|
||||||
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, 0, 2, _IS_BOOL, 0)
|
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, 0, 2, _IS_BOOL, 0)
|
||||||
ZEND_ARG_TYPE_INFO(0, tableName, IS_STRING, 0)
|
ZEND_ARG_TYPE_INFO(0, tableName, IS_STRING, 0)
|
||||||
@ -46,6 +46,10 @@ ZEND_END_ARG_INFO()
|
|||||||
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, 0, 0, IS_LONG, 0)
|
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, 0, 0, IS_LONG, 0)
|
||||||
ZEND_END_ARG_INFO()
|
ZEND_END_ARG_INFO()
|
||||||
|
|
||||||
|
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_PDO_PGSql_Ext_pgsqlSetNoticeCallback, 0, 1, IS_VOID, 0)
|
||||||
|
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 1)
|
||||||
|
ZEND_END_ARG_INFO()
|
||||||
|
|
||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromArray);
|
||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyFromFile);
|
||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlCopyToArray);
|
||||||
@ -55,6 +59,7 @@ ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBOpen);
|
|||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlLOBUnlink);
|
||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetNotify);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetNotify);
|
||||||
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetPid);
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlGetPid);
|
||||||
|
ZEND_METHOD(PDO_PGSql_Ext, pgsqlSetNoticeCallback);
|
||||||
|
|
||||||
static const zend_function_entry class_PDO_PGSql_Ext_methods[] = {
|
static const zend_function_entry class_PDO_PGSql_Ext_methods[] = {
|
||||||
ZEND_ME(PDO_PGSql_Ext, pgsqlCopyFromArray, arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, ZEND_ACC_PUBLIC)
|
ZEND_ME(PDO_PGSql_Ext, pgsqlCopyFromArray, arginfo_class_PDO_PGSql_Ext_pgsqlCopyFromArray, ZEND_ACC_PUBLIC)
|
||||||
@ -66,5 +71,6 @@ static const zend_function_entry class_PDO_PGSql_Ext_methods[] = {
|
|||||||
ZEND_ME(PDO_PGSql_Ext, pgsqlLOBUnlink, arginfo_class_PDO_PGSql_Ext_pgsqlLOBUnlink, ZEND_ACC_PUBLIC)
|
ZEND_ME(PDO_PGSql_Ext, pgsqlLOBUnlink, arginfo_class_PDO_PGSql_Ext_pgsqlLOBUnlink, ZEND_ACC_PUBLIC)
|
||||||
ZEND_ME(PDO_PGSql_Ext, pgsqlGetNotify, arginfo_class_PDO_PGSql_Ext_pgsqlGetNotify, ZEND_ACC_PUBLIC)
|
ZEND_ME(PDO_PGSql_Ext, pgsqlGetNotify, arginfo_class_PDO_PGSql_Ext_pgsqlGetNotify, ZEND_ACC_PUBLIC)
|
||||||
ZEND_ME(PDO_PGSql_Ext, pgsqlGetPid, arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, ZEND_ACC_PUBLIC)
|
ZEND_ME(PDO_PGSql_Ext, pgsqlGetPid, arginfo_class_PDO_PGSql_Ext_pgsqlGetPid, ZEND_ACC_PUBLIC)
|
||||||
|
ZEND_ME(PDO_PGSql_Ext, pgsqlSetNoticeCallback, arginfo_class_PDO_PGSql_Ext_pgsqlSetNoticeCallback, ZEND_ACC_PUBLIC)
|
||||||
ZEND_FE_END
|
ZEND_FE_END
|
||||||
};
|
};
|
||||||
|
@ -46,6 +46,7 @@ typedef struct {
|
|||||||
bool disable_native_prepares; /* deprecated since 5.6 */
|
bool disable_native_prepares; /* deprecated since 5.6 */
|
||||||
bool disable_prepares;
|
bool disable_prepares;
|
||||||
HashTable *lob_streams;
|
HashTable *lob_streams;
|
||||||
|
zend_fcall_info_cache *notice_callback;
|
||||||
} pdo_pgsql_db_handle;
|
} pdo_pgsql_db_handle;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
23
ext/pdo_pgsql/tests/issue78621.inc
Normal file
23
ext/pdo_pgsql/tests/issue78621.inc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
|
||||||
|
require_once dirname(__FILE__) . '/config.inc';
|
||||||
|
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
|
||||||
|
|
||||||
|
attach($db);
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
$db->exec("set client_min_messages to notice");
|
||||||
|
$db->exec("create temporary table t (a varchar(3))");
|
||||||
|
$db->exec("create function hey() returns trigger as \$\$ begin new.a := 'oh'; raise notice 'I tampered your data, did you know?'; return new; end; \$\$ language plpgsql");
|
||||||
|
$db->exec("create trigger hop before insert on t for each row execute procedure hey()");
|
||||||
|
$db->exec("insert into t values ('ah')");
|
||||||
|
attach($db, 'Re');
|
||||||
|
$db->exec("delete from t");
|
||||||
|
$db->exec("insert into t values ('ah')");
|
||||||
|
$db->pgsqlSetNoticeCallback(null);
|
||||||
|
$db->exec("delete from t");
|
||||||
|
$db->exec("insert into t values ('ah')");
|
||||||
|
var_dump($db->query("select * from t")->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
echo "Done\n";
|
||||||
|
$db->rollback();
|
||||||
|
?>
|
30
ext/pdo_pgsql/tests/issue78621.phpt
Normal file
30
ext/pdo_pgsql/tests/issue78621.phpt
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
--TEST--
|
||||||
|
pgsqlSetNoticeCallback catches Postgres "raise notice".
|
||||||
|
--SKIPIF--
|
||||||
|
<?php
|
||||||
|
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
|
||||||
|
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
|
||||||
|
require_once dirname(__FILE__) . '/config.inc';
|
||||||
|
PDOTest::skip();
|
||||||
|
?>
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
function disp($message) { echo trim($message)."\n"; }
|
||||||
|
function dispRe($message) { echo "Re".trim($message)."\n"; }
|
||||||
|
function attach($db, $prefix = '')
|
||||||
|
{
|
||||||
|
$db->pgsqlSetNoticeCallback('disp'.$prefix);
|
||||||
|
}
|
||||||
|
require dirname(__FILE__) . '/issue78621.inc';
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
NOTICE: I tampered your data, did you know?
|
||||||
|
ReNOTICE: I tampered your data, did you know?
|
||||||
|
array(1) {
|
||||||
|
[0]=>
|
||||||
|
array(1) {
|
||||||
|
["a"]=>
|
||||||
|
string(2) "oh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done
|
58
ext/pdo_pgsql/tests/issue78621_closure.phpt
Normal file
58
ext/pdo_pgsql/tests/issue78621_closure.phpt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
--TEST--
|
||||||
|
pgsqlSetNoticeCallback catches Postgres "raise notice".
|
||||||
|
--SKIPIF--
|
||||||
|
<?php
|
||||||
|
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
|
||||||
|
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
|
||||||
|
require_once dirname(__FILE__) . '/config.inc';
|
||||||
|
PDOTest::skip();
|
||||||
|
?>
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
function disp($message) { echo trim($message)."\n"; }
|
||||||
|
function attach($db, $prefix = '')
|
||||||
|
{
|
||||||
|
global $flavor;
|
||||||
|
switch($flavor)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
$db->pgsqlSetNoticeCallback(function($message) use($prefix) { echo $prefix.trim($message)."\n"; });
|
||||||
|
// https://github.com/php/php-src/pull/4823#pullrequestreview-335623806
|
||||||
|
$eraseCallbackMemoryHere = (object)[1];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
$closure = function($message) use($prefix) { echo $prefix.'('.get_class($this).')'.trim($message)."\n"; };
|
||||||
|
$db->pgsqlSetNoticeCallback($closure->bindTo(new \stdClass));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "Testing with a simple inline closure:\n";
|
||||||
|
$flavor = 0;
|
||||||
|
require dirname(__FILE__) . '/issue78621.inc';
|
||||||
|
echo "Testing with a postbound closure object:\n";
|
||||||
|
++$flavor;
|
||||||
|
require dirname(__FILE__) . '/issue78621.inc';
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
Testing with a simple inline closure:
|
||||||
|
NOTICE: I tampered your data, did you know?
|
||||||
|
ReNOTICE: I tampered your data, did you know?
|
||||||
|
array(1) {
|
||||||
|
[0]=>
|
||||||
|
array(1) {
|
||||||
|
["a"]=>
|
||||||
|
string(2) "oh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done
|
||||||
|
Testing with a postbound closure object:
|
||||||
|
(stdClass)NOTICE: I tampered your data, did you know?
|
||||||
|
Re(stdClass)NOTICE: I tampered your data, did you know?
|
||||||
|
array(1) {
|
||||||
|
[0]=>
|
||||||
|
array(1) {
|
||||||
|
["a"]=>
|
||||||
|
string(2) "oh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done
|
65
ext/pdo_pgsql/tests/issue78621_method.phpt
Normal file
65
ext/pdo_pgsql/tests/issue78621_method.phpt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
--TEST--
|
||||||
|
pgsqlSetNoticeCallback catches Postgres "raise notice".
|
||||||
|
--SKIPIF--
|
||||||
|
<?php
|
||||||
|
if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
|
||||||
|
require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
|
||||||
|
require_once dirname(__FILE__) . '/config.inc';
|
||||||
|
PDOTest::skip();
|
||||||
|
?>
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
public function disp($message) { echo trim($message)."\n"; }
|
||||||
|
public function dispRe($message) { echo "Re".trim($message)."\n"; }
|
||||||
|
public function __call(string $method, array $args)
|
||||||
|
{
|
||||||
|
$realMethod = strtr($method, [ 'whatever' => 'disp' ]);
|
||||||
|
echo "$method trampoline for $realMethod\n";
|
||||||
|
return call_user_func_array([ $this, $realMethod ], $args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$logger = new Logger();
|
||||||
|
function attach($db, $prefix = '')
|
||||||
|
{
|
||||||
|
global $logger;
|
||||||
|
global $flavor;
|
||||||
|
switch($flavor)
|
||||||
|
{
|
||||||
|
case 0: $db->pgsqlSetNoticeCallback([ $logger, 'disp'.$prefix ]); break;
|
||||||
|
case 1: $db->pgsqlSetNoticeCallback([ $logger, 'whatever'.$prefix ]); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "Testing with method explicitely plugged:\n";
|
||||||
|
$flavor = 0;
|
||||||
|
require dirname(__FILE__) . '/issue78621.inc';
|
||||||
|
echo "Testing with a bit of magic:\n";
|
||||||
|
++$flavor;
|
||||||
|
require dirname(__FILE__) . '/issue78621.inc';
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
Testing with method explicitely plugged:
|
||||||
|
NOTICE: I tampered your data, did you know?
|
||||||
|
ReNOTICE: I tampered your data, did you know?
|
||||||
|
array(1) {
|
||||||
|
[0]=>
|
||||||
|
array(1) {
|
||||||
|
["a"]=>
|
||||||
|
string(2) "oh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done
|
||||||
|
Testing with a bit of magic:
|
||||||
|
whatever trampoline for disp
|
||||||
|
NOTICE: I tampered your data, did you know?
|
||||||
|
whateverRe trampoline for dispRe
|
||||||
|
ReNOTICE: I tampered your data, did you know?
|
||||||
|
array(1) {
|
||||||
|
[0]=>
|
||||||
|
array(1) {
|
||||||
|
["a"]=>
|
||||||
|
string(2) "oh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Done
|
Loading…
Reference in New Issue
Block a user