2023-05-12 15:06:01 +02:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Messenger\Bridge\Doctrine\Transport ;
use Doctrine\DBAL\Schema\Table ;
/**
* Uses PostgreSQL LISTEN / NOTIFY to push messages to workers .
*
2024-05-17 20:26:01 +02:00
* If you do not want to use the LISTEN mechanism , set the `use_notify` option to `false` when calling DoctrineTransportFactory :: createTransport .
*
2023-05-12 15:06:01 +02:00
* @ internal
*
* @ author Kévin Dunglas < dunglas @ gmail . com >
*/
final class PostgreSqlConnection extends Connection
{
/**
* * check_delayed_interval : The interval to check for delayed messages , in milliseconds . Set to 0 to disable checks . Default : 60000 ( 1 minute )
* * get_notify_timeout : The length of time to wait for a response when calling PDO :: pgsqlGetNotify , in milliseconds . Default : 0.
*/
protected const DEFAULT_OPTIONS = parent :: DEFAULT_OPTIONS + [
'check_delayed_interval' => 60000 ,
'get_notify_timeout' => 0 ,
];
public function __sleep () : array
{
throw new \BadMethodCallException ( 'Cannot serialize ' . __CLASS__ );
}
public function __wakeup ()
{
throw new \BadMethodCallException ( 'Cannot unserialize ' . __CLASS__ );
}
public function __destruct ()
{
$this -> unlisten ();
}
public function reset ()
{
parent :: reset ();
$this -> unlisten ();
}
public function get () : ? array
{
if ( null === $this -> queueEmptiedAt ) {
return parent :: get ();
}
// This is secure because the table name must be a valid identifier:
// https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
$this -> executeStatement ( sprintf ( 'LISTEN "%s"' , $this -> configuration [ 'table_name' ]));
2024-05-17 20:26:01 +02:00
// The condition should be removed once support for DBAL <3.3 is dropped
2023-05-12 15:06:01 +02:00
if ( method_exists ( $this -> driverConnection , 'getNativeConnection' )) {
$wrappedConnection = $this -> driverConnection -> getNativeConnection ();
} else {
$wrappedConnection = $this -> driverConnection ;
while ( method_exists ( $wrappedConnection , 'getWrappedConnection' )) {
$wrappedConnection = $wrappedConnection -> getWrappedConnection ();
}
}
$notification = $wrappedConnection -> pgsqlGetNotify ( \PDO :: FETCH_ASSOC , $this -> configuration [ 'get_notify_timeout' ]);
if (
// no notifications, or for another table or queue
( false === $notification || $notification [ 'message' ] !== $this -> configuration [ 'table_name' ] || $notification [ 'payload' ] !== $this -> configuration [ 'queue_name' ]) &&
// delayed messages
( microtime ( true ) * 1000 - $this -> queueEmptiedAt < $this -> configuration [ 'check_delayed_interval' ])
) {
usleep ( 1000 );
return null ;
}
return parent :: get ();
}
public function setup () : void
{
parent :: setup ();
$this -> executeStatement ( implode ( " \n " , $this -> getTriggerSql ()));
}
/**
* @ return string []
*/
public function getExtraSetupSqlForTable ( Table $createdTable ) : array
{
if ( ! $createdTable -> hasOption ( self :: TABLE_OPTION_NAME )) {
return [];
}
if ( $createdTable -> getOption ( self :: TABLE_OPTION_NAME ) !== $this -> configuration [ 'table_name' ]) {
return [];
}
return $this -> getTriggerSql ();
}
private function getTriggerSql () : array
{
$functionName = $this -> createTriggerFunctionName ();
return [
// create trigger function
sprintf ( <<< 'SQL'
CREATE OR REPLACE FUNCTION % 1 $s () RETURNS TRIGGER AS $ $
BEGIN
PERFORM pg_notify ( '%2$s' , NEW . queue_name :: text );
RETURN NEW ;
END ;
$ $ LANGUAGE plpgsql ;
SQL
, $functionName , $this -> configuration [ 'table_name' ]),
// register trigger
sprintf ( 'DROP TRIGGER IF EXISTS notify_trigger ON %s;' , $this -> configuration [ 'table_name' ]),
sprintf ( 'CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON %1$s FOR EACH ROW EXECUTE PROCEDURE %2$s();' , $this -> configuration [ 'table_name' ], $functionName ),
];
}
private function createTriggerFunctionName () : string
{
$tableConfig = explode ( '.' , $this -> configuration [ 'table_name' ]);
if ( 1 === \count ( $tableConfig )) {
return sprintf ( 'notify_%1$s' , $tableConfig [ 0 ]);
}
return sprintf ( '%1$s.notify_%2$s' , $tableConfig [ 0 ], $tableConfig [ 1 ]);
}
private function unlisten ()
{
$this -> executeStatement ( sprintf ( 'UNLISTEN "%s"' , $this -> configuration [ 'table_name' ]));
}
}