From f939e3bbdc23696f7321c022214d7c7111d915cf Mon Sep 17 00:00:00 2001 From: Dan Barker Date: Wed, 9 Apr 2025 10:40:57 +0100 Subject: [PATCH 1/2] Add retry mechanism and more detailed logging for PDOException/AdapterException in ConnectionFactory --- src/Propel/Runtime/Adapter/Pdo/PdoAdapter.php | 6 ++ .../Runtime/Connection/ConnectionFactory.php | 63 +++++++++++++------ 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Propel/Runtime/Adapter/Pdo/PdoAdapter.php b/src/Propel/Runtime/Adapter/Pdo/PdoAdapter.php index bf1a99d2e5..55aa511e09 100644 --- a/src/Propel/Runtime/Adapter/Pdo/PdoAdapter.php +++ b/src/Propel/Runtime/Adapter/Pdo/PdoAdapter.php @@ -79,6 +79,12 @@ public function getConnection(array $params): PdoConnection $con = new PdoConnection($dsn, $user, $password, $driverOptions); $this->initConnection($con, isset($params['settings']) && is_array($params['settings']) ? $params['settings'] : []); } catch (PDOException $e) { + // Detailed error logging + error_log(sprintf('PDO Connection Error: DSN: %s, Error: %s, Code: %s', + preg_replace('/password=.*?;/', 'password=***;', $dsn), + $e->getMessage(), + $e->getCode() + )); throw new AdapterException('Unable to open PDO connection', 0, $e); } diff --git a/src/Propel/Runtime/Connection/ConnectionFactory.php b/src/Propel/Runtime/Connection/ConnectionFactory.php index 7319719ad4..b7cb116df9 100644 --- a/src/Propel/Runtime/Connection/ConnectionFactory.php +++ b/src/Propel/Runtime/Connection/ConnectionFactory.php @@ -51,28 +51,53 @@ public static function create( } else { $connectionClass = $defaultConnectionClass; } - try { - $adapterConnection = $adapter->getConnection($configuration); - } catch (AdapterException $e) { - throw new ConnectionException('Unable to open connection', 0, $e); - } - /** @var \Propel\Runtime\Connection\ConnectionInterface $connection */ - $connection = new $connectionClass($adapterConnection); - - // load any connection options from the config file - // connection attributes are those PDO flags that have to be set on the initialized connection - if (isset($configuration['attributes']) && is_array($configuration['attributes'])) { - foreach ($configuration['attributes'] as $option => $value) { - if (is_string($value) && strpos($value, '::') !== false) { - if (!defined($value)) { - throw new InvalidArgumentException(sprintf('Invalid class constant specified "%s" while processing connection attributes for datasource "%s"', $value, $connection->getName())); + + $maxRetries = 2; + $retryCount = 0; + $lastException = null; + + while ($retryCount <= $maxRetries) { + try { + $adapterConnection = $adapter->getConnection($configuration); + + /** @var \Propel\Runtime\Connection\ConnectionInterface $connection */ + $connection = new $connectionClass($adapterConnection); + + // load any connection options from the config file + // connection attributes are those PDO flags that have to be set on the initialized connection + if (isset($configuration['attributes']) && is_array($configuration['attributes'])) { + foreach ($configuration['attributes'] as $option => $value) { + if (is_string($value) && strpos($value, '::') !== false) { + if (!defined($value)) { + throw new InvalidArgumentException(sprintf('Invalid class constant specified "%s" while processing connection attributes for datasource "%s"', $value, $connection->getName())); + } + $value = constant($value); + } + $connection->setAttribute($option, $value); } - $value = constant($value); } - $connection->setAttribute($option, $value); + + return $connection; + + } catch (AdapterException $e) { + $lastException = $e; + $retryCount++; + + // Log the connection attempt failure + error_log(sprintf('Propel connection attempt %d/%d failed: %s', + $retryCount, + $maxRetries + 1, + $e->getMessage() . ' - ' . ($e->getPrevious() ? $e->getPrevious()->getMessage() : '') + )); + + if ($retryCount <= $maxRetries) { + // Wait before retrying + usleep(100000); // 100ms + } } } - - return $connection; + + // If we get here, all retries have failed + throw new ConnectionException('Unable to open connection after ' . ($maxRetries + 1) . ' attempts', 0, $lastException); } } From 3776f3784cbb4ee9d27d5009e2e551b089794da3 Mon Sep 17 00:00:00 2001 From: Dan Barker Date: Wed, 1 Jul 2026 21:36:11 +0100 Subject: [PATCH 2/2] fix(ModelCriteria): accept a Criteria in basePreUpdate()/preUpdate() (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update() and doUpdate() are both written to accept either an array of values or a Criteria (doUpdate has an explicit `$updateValues instanceof Criteria` branch, and update()'s guard allows both). But basePreUpdate()/preUpdate() typed the parameter `array &$values`, so passing a Criteria fatals: TypeError: ModelCriteria::basePreUpdate(): Argument #1 ($values) must be of type array, Propel\Runtime\ActiveQuery\Criteria given This breaks the generated ON DELETE SET NULL emulation (doOnDeleteSetNull builds a Criteria and calls update()) for any model that does not get a behavior-generated basePreUpdate() override — QueryBuilder emits that override with an *untyped* `&$values`, so behavior-ful models (e.g. versionable) already work while timestampable-only / plain models hit the typed runtime method and 500. Drop the `array` hint so the runtime matches the generator (and upstream), letting both forms flow through to doUpdate(). Co-authored-by: Dan Barker Co-authored-by: Claude Opus 4.8 (1M context) --- src/Propel/Runtime/ActiveQuery/ModelCriteria.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Propel/Runtime/ActiveQuery/ModelCriteria.php b/src/Propel/Runtime/ActiveQuery/ModelCriteria.php index cf4caf25d4..2860315773 100644 --- a/src/Propel/Runtime/ActiveQuery/ModelCriteria.php +++ b/src/Propel/Runtime/ActiveQuery/ModelCriteria.php @@ -1945,25 +1945,31 @@ public function deleteAll(?ConnectionInterface $con = null): int /** * Code to execute before every UPDATE statement * - * @param array $values The associative array of columns and values for the update + * $values is an associative array of columns and values for a normal + * update(), or a Criteria when update() is called with one (as the + * generated ON DELETE SET NULL emulation does). The parameter is left + * untyped so both forms reach doUpdate(), which handles each — matching the + * untyped basePreUpdate() the QueryBuilder generates for behavior hooks. + * + * @param array|\Propel\Runtime\ActiveQuery\Criteria $values The associative array of columns and values for the update * @param \Propel\Runtime\Connection\ConnectionInterface $con The connection object used by the query * @param bool $forceIndividualSaves If false (default), the resulting call is a Criteria::doUpdate(), otherwise it is a series of save() calls on all the found objects * * @return int|null */ - protected function basePreUpdate(array &$values, ConnectionInterface $con, bool $forceIndividualSaves = false): ?int + protected function basePreUpdate(&$values, ConnectionInterface $con, bool $forceIndividualSaves = false): ?int { return $this->preUpdate($values, $con, $forceIndividualSaves); } /** - * @param array $values + * @param array|\Propel\Runtime\ActiveQuery\Criteria $values * @param \Propel\Runtime\Connection\ConnectionInterface $con * @param bool $forceIndividualSaves * * @return int|null */ - protected function preUpdate(array &$values, ConnectionInterface $con, bool $forceIndividualSaves = false): ?int + protected function preUpdate(&$values, ConnectionInterface $con, bool $forceIndividualSaves = false): ?int { return null; }