Skip to content

fix(payment): handle silent PDO errors - #753

Open
Ibochkarev wants to merge 4 commits into
modx-pro:betafrom
Ibochkarev:cursor/fix-payment-attempt-silent-errors-2630
Open

Ibochkarev wants to merge 4 commits into
modx-pro:betafrom
Ibochkarev:cursor/fix-payment-attempt-silent-errors-2630

Conversation

@Ibochkarev

Copy link
Copy Markdown
Member

Описание

PdoPaymentAttemptStore теперь проверяет результат PDOStatement::execute() и не проглатывает SQL-ошибки при подключении MODX с PDO::ERRMODE_SILENT.

  • update() и параметризованные чтения выбрасывают исключение с errorInfo();
  • create() отличает ошибку уникальности и возвращает существующую попытку;
  • повторное чтение дубля использует current read с LOCK IN SHARE MODE: оно видит конкурентно закоммиченную запись внутри старого REPEATABLE READ snapshot и не повышает shared-lock до exclusive;
  • остальные ошибки вставки выбрасываются с диагностикой SQL;
  • добавлены MySQL-регрессионные тесты с ERRMODE_SILENT, включая snapshot и совместимость блокировок на нескольких PDO-соединениях.

Тип изменений

  • Исправление бага (non-breaking change)
  • Новая функциональность (non-breaking change)
  • Breaking change (изменение, ломающее обратную совместимость)
  • Рефакторинг (без изменения функциональности)
  • Документация
  • Другое (опишите):

Связанные Issues

Closes #752

Как это было протестировано?

MS3_TEST_MYSQL_DSN='mysql:host=127.0.0.1;port=3306;dbname=ms3_test;charset=utf8mb4' \
MS3_TEST_MYSQL_USER=ms3 MS3_TEST_MYSQL_PASSWORD=ms3 \
composer ci:php

./vendor/bin/phpstan analyse -c ../../../phpstan.neon --memory-limit=1G \
  src/Services/Payment/PdoPaymentAttemptStore.php \
  tests/support/MysqlTestConnection.php \
  tests/Integration/Mysql/PdoPaymentAttemptStoreMysqlTest.php

Результат после rebase: 113 smoke-тестов и 644 PHPUnit-теста (2426 assertions) прошли; PHPStan — без ошибок. В общем наборе остаётся 1 существующая deprecation.

  • Ручное тестирование
  • Автоматические тесты (composer ci:php / composer test, npm run lint:ci, composer stan / GitHub Actions CI)
  • Тестирование на разных версиях PHP/MODX

Конфигурация тестирования:

  • MiniShop3: upstream beta (ac8e267e)
  • MODX: не применимо для теста PDO-хранилища
  • PHP: 8.3.6, MariaDB 10.11

Скриншоты (если применимо)

Не применимо.

Чеклист

  • Код соответствует стилю проекта
  • Добавлены/обновлены комментарии в сложных местах
  • Изменения не ломают существующую функциональность
  • Лексиконы добавлены на двух языках (ru/en) — не применимо
  • PHPStan проходит без новых ошибок (composer stan / CI job PHPStan)
  • ESLint проходит без ошибок (npm run lint:ci для Vue) — Vue не изменялся
  • Обновлён CHANGELOG.md (для значимых изменений)

@Ibochkarev
Ibochkarev requested a review from biz87 September 21, 2026 09:56
@Ibochkarev Ibochkarev added the bug Something isn't working label Sep 21, 2026
@Ibochkarev Ibochkarev changed the title Cursor/fix payment attempt silent errors 2630 fix(payment): handle silent PDO errors Sep 21, 2026
@biz87

biz87 commented Sep 21, 2026

Copy link
Copy Markdown
Member

Спасибо. executeStatement() по образцу PdoShipmentStore на месте, гейт зелёный: PHPUnit 659, PHPStan версии CI без ошибок. Новые MySQL-тесты прогнал на живой MariaDB 10.6 (MS3_TEST_MYSQL_DSN на dev-базу) — 6 из 6 проходят, включая snapshot и совместимость блокировок.

Проверил на dev сам сценарий из #752 — вызовы хранилища на $modx->pdo (ERRMODE_SILENT):

Вызов beta c6626e4f PR
update() со слишком длинным currency молча возвращает старую строку RuntimeException: Payment attempt store statement failed: 22001 | 1406 | Data too long for column 'currency'
create() с тем же (payment_method_id, provider, external_id) RuntimeException('Failed to load created payment attempt') возвращает существующую попытку, тот же id
recordEvent() дважды с одним provider_event_id true, true true, true
provider_event_id длиннее 191 символа hasEvent=false, recordEvent=true, строки нет так же

Первые две строки закрыты. Возвращаю из-за последних двух.

recordEvent() и hasEvent() остались на сыром execute()

Оба метода не переведены на executeStatement(), а при ERRMODE_SILENT исключения не будет — execute() просто вернёт false:

  • recordEvent() при дубле возвращает true вместо false. Ветка if (!$this->recordEvent(...)) в writeWithEvent() не срабатывает никогда;
  • если вставка события не прошла по любой другой причине (проверял на provider_event_id длиной 300 символов при varchar(191)), метод тоже возвращает true: попытка обновлена и закоммичена, а строки события нет. Повторный вебхук с тем же идентификатором увидит hasEvent = false и применит изменение ещё раз;
  • hasEvent() при ошибке запроса вернёт false — то есть «события не было» — с тем же результатом.

Сейчас от двойной обработки спасает только hasEvent() под FOR UPDATE внутри транзакции, а это ровно тот же молчаливый путь. Правка короткая: провести оба метода через executeStatement()recordEvent() с allowDuplicate: true (тогда дубль честно вернёт false), hasEvent() с allowDuplicate: false. Плюс тест на повтор recordEvent() в MySQL-наборе.

Мелочь

findDuplicateWithSharedLock() использует LOCK IN SHARE MODE. Синтаксис работает и в MySQL 8, и в MariaDB, но в MySQL 8 каноничная форма — FOR SHARE. Если хочется единообразия с FOR UPDATE в writeWithEvent(), можно заменить.

Co-authored-by: Bochkarev Ivan <ivanx86@gmail.com>
Co-authored-by: Bochkarev Ivan <ivanx86@gmail.com>
Co-authored-by: Bochkarev Ivan <ivanx86@gmail.com>
Co-authored-by: Bochkarev Ivan <ivanx86@gmail.com>
@cursor
cursor Bot force-pushed the cursor/fix-payment-attempt-silent-errors-2630 branch from 58c69b0 to 0469d64 Compare September 21, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PdoPaymentAttemptStore: ошибки записи проглатываются на соединении MODX (ERRMODE_SILENT)

2 participants