vendor/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php line 155

Open in your IDE?
  1. <?php
  2. namespace Doctrine\DBAL\Connections;
  3. use Doctrine\Common\EventManager;
  4. use Doctrine\DBAL\Configuration;
  5. use Doctrine\DBAL\Connection;
  6. use Doctrine\DBAL\Driver;
  7. use Doctrine\DBAL\Driver\Connection as DriverConnection;
  8. use Doctrine\DBAL\Driver\Exception as DriverException;
  9. use Doctrine\DBAL\DriverManager;
  10. use Doctrine\DBAL\Event\ConnectionEventArgs;
  11. use Doctrine\DBAL\Events;
  12. use Doctrine\DBAL\Exception;
  13. use Doctrine\DBAL\Statement;
  14. use Doctrine\Deprecations\Deprecation;
  15. use InvalidArgumentException;
  16. use SensitiveParameter;
  17. use function array_rand;
  18. use function count;
  19. /**
  20.  * Primary-Replica Connection
  21.  *
  22.  * Connection can be used with primary-replica setups.
  23.  *
  24.  * Important for the understanding of this connection should be how and when
  25.  * it picks the replica or primary.
  26.  *
  27.  * 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
  28.  *    or 'executeQuery' is used.
  29.  * 2. Primary picked when 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
  30.  *    'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'prepare' is called.
  31.  * 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
  32.  * 4. One replica connection is randomly picked ONCE during a request.
  33.  *
  34.  * ATTENTION: You can write to the replica with this connection if you execute a write query without
  35.  * opening up a transaction. For example:
  36.  *
  37.  *      $conn = DriverManager::getConnection(...);
  38.  *      $conn->executeQuery("DELETE FROM table");
  39.  *
  40.  * Be aware that Connection#executeQuery is a method specifically for READ
  41.  * operations only.
  42.  *
  43.  * Use Connection#executeStatement for any SQL statement that changes/updates
  44.  * state in the database (UPDATE, INSERT, DELETE or DDL statements).
  45.  *
  46.  * This connection is limited to replica operations using the
  47.  * Connection#executeQuery operation only, because it wouldn't be compatible
  48.  * with the ORM or SchemaManager code otherwise. Both use all the other
  49.  * operations in a context where writes could happen to a replica, which makes
  50.  * this restricted approach necessary.
  51.  *
  52.  * You can manually connect to the primary at any time by calling:
  53.  *
  54.  *      $conn->ensureConnectedToPrimary();
  55.  *
  56.  * Instantiation through the DriverManager looks like:
  57.  *
  58.  * @psalm-import-type Params from DriverManager
  59.  * @example
  60.  *
  61.  * $conn = DriverManager::getConnection(array(
  62.  *    'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection',
  63.  *    'driver' => 'pdo_mysql',
  64.  *    'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
  65.  *    'replica' => array(
  66.  *        array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''),
  67.  *        array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''),
  68.  *    )
  69.  * ));
  70.  *
  71.  * You can also pass 'driverOptions' and any other documented option to each of this drivers
  72.  * to pass additional information.
  73.  */
  74. class PrimaryReadReplicaConnection extends Connection
  75. {
  76.     /**
  77.      * Primary and Replica connection (one of the randomly picked replicas).
  78.      *
  79.      * @var DriverConnection[]|null[]
  80.      */
  81.     protected $connections = ['primary' => null'replica' => null];
  82.     /**
  83.      * You can keep the replica connection and then switch back to it
  84.      * during the request if you know what you are doing.
  85.      *
  86.      * @var bool
  87.      */
  88.     protected $keepReplica false;
  89.     /**
  90.      * Creates Primary Replica Connection.
  91.      *
  92.      * @internal The connection can be only instantiated by the driver manager.
  93.      *
  94.      * @param array<string,mixed> $params
  95.      * @psalm-param Params $params
  96.      *
  97.      * @throws Exception
  98.      * @throws InvalidArgumentException
  99.      */
  100.     public function __construct(
  101.         array $params,
  102.         Driver $driver,
  103.         ?Configuration $config null,
  104.         ?EventManager $eventManager null
  105.     ) {
  106.         if (! isset($params['replica'], $params['primary'])) {
  107.             throw new InvalidArgumentException('primary or replica configuration missing');
  108.         }
  109.         if (count($params['replica']) === 0) {
  110.             throw new InvalidArgumentException('You have to configure at least one replica.');
  111.         }
  112.         if (isset($params['driver'])) {
  113.             $params['primary']['driver'] = $params['driver'];
  114.             foreach ($params['replica'] as $replicaKey => $replica) {
  115.                 $params['replica'][$replicaKey]['driver'] = $params['driver'];
  116.             }
  117.         }
  118.         $this->keepReplica = (bool) ($params['keepReplica'] ?? false);
  119.         parent::__construct($params$driver$config$eventManager);
  120.     }
  121.     /**
  122.      * Checks if the connection is currently towards the primary or not.
  123.      */
  124.     public function isConnectedToPrimary(): bool
  125.     {
  126.         return $this->_conn !== null && $this->_conn === $this->connections['primary'];
  127.     }
  128.     /**
  129.      * @param string|null $connectionName
  130.      *
  131.      * @return bool
  132.      */
  133.     public function connect($connectionName null)
  134.     {
  135.         if ($connectionName !== null) {
  136.             throw new InvalidArgumentException(
  137.                 'Passing a connection name as first argument is not supported anymore.'
  138.                     ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.',
  139.             );
  140.         }
  141.         return $this->performConnect();
  142.     }
  143.     protected function performConnect(?string $connectionName null): bool
  144.     {
  145.         $requestedConnectionChange = ($connectionName !== null);
  146.         $connectionName            $connectionName ?? 'replica';
  147.         if ($connectionName !== 'replica' && $connectionName !== 'primary') {
  148.             throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
  149.         }
  150.         // If we have a connection open, and this is not an explicit connection
  151.         // change request, then abort right here, because we are already done.
  152.         // This prevents writes to the replica in case of "keepReplica" option enabled.
  153.         if ($this->_conn !== null && ! $requestedConnectionChange) {
  154.             return false;
  155.         }
  156.         $forcePrimaryAsReplica false;
  157.         if ($this->getTransactionNestingLevel() > 0) {
  158.             $connectionName        'primary';
  159.             $forcePrimaryAsReplica true;
  160.         }
  161.         if (isset($this->connections[$connectionName])) {
  162.             $this->_conn $this->connections[$connectionName];
  163.             if ($forcePrimaryAsReplica && ! $this->keepReplica) {
  164.                 $this->connections['replica'] = $this->_conn;
  165.             }
  166.             return false;
  167.         }
  168.         if ($connectionName === 'primary') {
  169.             $this->connections['primary'] = $this->_conn $this->connectTo($connectionName);
  170.             // Set replica connection to primary to avoid invalid reads
  171.             if (! $this->keepReplica) {
  172.                 $this->connections['replica'] = $this->connections['primary'];
  173.             }
  174.         } else {
  175.             $this->connections['replica'] = $this->_conn $this->connectTo($connectionName);
  176.         }
  177.         if ($this->_eventManager->hasListeners(Events::postConnect)) {
  178.             Deprecation::trigger(
  179.                 'doctrine/dbal',
  180.                 'https://github.com/doctrine/dbal/issues/5784',
  181.                 'Subscribing to %s events is deprecated. Implement a middleware instead.',
  182.                 Events::postConnect,
  183.             );
  184.             $eventArgs = new ConnectionEventArgs($this);
  185.             $this->_eventManager->dispatchEvent(Events::postConnect$eventArgs);
  186.         }
  187.         return true;
  188.     }
  189.     /**
  190.      * Connects to the primary node of the database cluster.
  191.      *
  192.      * All following statements after this will be executed against the primary node.
  193.      */
  194.     public function ensureConnectedToPrimary(): bool
  195.     {
  196.         return $this->performConnect('primary');
  197.     }
  198.     /**
  199.      * Connects to a replica node of the database cluster.
  200.      *
  201.      * All following statements after this will be executed against the replica node,
  202.      * unless the keepReplica option is set to false and a primary connection
  203.      * was already opened.
  204.      */
  205.     public function ensureConnectedToReplica(): bool
  206.     {
  207.         return $this->performConnect('replica');
  208.     }
  209.     /**
  210.      * Connects to a specific connection.
  211.      *
  212.      * @param string $connectionName
  213.      *
  214.      * @return DriverConnection
  215.      *
  216.      * @throws Exception
  217.      */
  218.     protected function connectTo($connectionName)
  219.     {
  220.         $params $this->getParams();
  221.         $connectionParams $this->chooseConnectionConfiguration($connectionName$params);
  222.         try {
  223.             return $this->_driver->connect($connectionParams);
  224.         } catch (DriverException $e) {
  225.             throw $this->convertException($e);
  226.         }
  227.     }
  228.     /**
  229.      * @param string  $connectionName
  230.      * @param mixed[] $params
  231.      *
  232.      * @return mixed
  233.      */
  234.     protected function chooseConnectionConfiguration(
  235.         $connectionName,
  236.         #[SensitiveParameter]
  237.         $params
  238.     ) {
  239.         if ($connectionName === 'primary') {
  240.             return $params['primary'];
  241.         }
  242.         $config $params['replica'][array_rand($params['replica'])];
  243.         if (! isset($config['charset']) && isset($params['primary']['charset'])) {
  244.             $config['charset'] = $params['primary']['charset'];
  245.         }
  246.         return $config;
  247.     }
  248.     /**
  249.      * {@inheritDoc}
  250.      */
  251.     public function executeStatement($sql, array $params = [], array $types = [])
  252.     {
  253.         $this->ensureConnectedToPrimary();
  254.         return parent::executeStatement($sql$params$types);
  255.     }
  256.     /**
  257.      * {@inheritDoc}
  258.      */
  259.     public function beginTransaction()
  260.     {
  261.         $this->ensureConnectedToPrimary();
  262.         return parent::beginTransaction();
  263.     }
  264.     /**
  265.      * {@inheritDoc}
  266.      */
  267.     public function commit()
  268.     {
  269.         $this->ensureConnectedToPrimary();
  270.         return parent::commit();
  271.     }
  272.     /**
  273.      * {@inheritDoc}
  274.      */
  275.     public function rollBack()
  276.     {
  277.         $this->ensureConnectedToPrimary();
  278.         return parent::rollBack();
  279.     }
  280.     /**
  281.      * {@inheritDoc}
  282.      */
  283.     public function close()
  284.     {
  285.         unset($this->connections['primary'], $this->connections['replica']);
  286.         parent::close();
  287.         $this->_conn       null;
  288.         $this->connections = ['primary' => null'replica' => null];
  289.     }
  290.     /**
  291.      * {@inheritDoc}
  292.      */
  293.     public function createSavepoint($savepoint)
  294.     {
  295.         $this->ensureConnectedToPrimary();
  296.         parent::createSavepoint($savepoint);
  297.     }
  298.     /**
  299.      * {@inheritDoc}
  300.      */
  301.     public function releaseSavepoint($savepoint)
  302.     {
  303.         $this->ensureConnectedToPrimary();
  304.         parent::releaseSavepoint($savepoint);
  305.     }
  306.     /**
  307.      * {@inheritDoc}
  308.      */
  309.     public function rollbackSavepoint($savepoint)
  310.     {
  311.         $this->ensureConnectedToPrimary();
  312.         parent::rollbackSavepoint($savepoint);
  313.     }
  314.     public function prepare(string $sql): Statement
  315.     {
  316.         $this->ensureConnectedToPrimary();
  317.         return parent::prepare($sql);
  318.     }
  319. }