Skip to content

Commit 00f32e8

Browse files
committed
Adds DB facade macro for transaction retries
Introduces a `DB::transactionWithRetry` macro to the DB facade and connection instances. This allows for easier and more readable integration of transaction retry logic directly through the database facade, simplifying existing transaction code. Includes documentation updates and necessary service provider registration.
1 parent 50d118a commit 00f32e8

File tree

5 files changed

+113
-1
lines changed

5 files changed

+113
-1
lines changed

.php-cs-fixer.cache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"php":"8.2.29","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"align_single_space_minimal"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"single_quote":true,"no_unused_imports":true,"no_superfluous_phpdoc_tags":true,"phpdoc_trim":true,"phpdoc_align":{"align":"left"},"blank_line_before_statement":{"statements":["return"]},"simplified_null_return":true,"void_return":true},"hashes":{"tests\/Unit\/DBTransactionRetryHelperTest.php":"27f37268e9ae100d356ca1c06c756616","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b","tests\/bootstrap.php":"4ae74313e457f6662f4831ee2140eb34","src\/Console\/StartRetryCommand.php":"b0e6c76186a59d1341fa6600e096f9a3","src\/Console\/StopRetryCommand.php":"ad3d7cbb9841006db54f12168c58d369","src\/Providers\/DatabaseTransactionRetryServiceProvider.php":"638c2f84c78a86c8e8c5e4daef07c46e","src\/Services\/TransactionRetrier.php":"504c45ab8315c7b3d60a64e0a52759af","src\/Support\/RetryToggle.php":"2516f4c290940019b1f1c069fec64be8","src\/Support\/BindingStringifier.php":"bbead2bae37761124652320cd28db412","src\/Support\/TransactionRetryLogWriter.php":"2bc60103437973d7b471ad6bc91c43cf","src\/Support\/TraceFormatter.php":"e640e32b17149f1cfec9a1d990f04c84","tests\/TestCase.php":"897cfbd81822f4b71075ccb1739df70d","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087"}}
1+
{"php":"8.2.29","version":"3.89.0:v3.89.0#4dd6768cb7558440d27d18f54909eee417317ce9","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"align_single_space_minimal"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":true,"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"sort_algorithm":"alpha"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"single_quote":true,"no_unused_imports":true,"no_superfluous_phpdoc_tags":true,"phpdoc_trim":true,"phpdoc_align":{"align":"left"},"blank_line_before_statement":{"statements":["return"]},"simplified_null_return":true,"void_return":true},"hashes":{"tests\/Unit\/DBTransactionRetryHelperTest.php":"22e94a33db107726ebdb9ab6c1794f38","tests\/Feature\/ExampleTest.php":"a1e5352ea369ad36f88f4f566c340371","tests\/Pest.php":"44a41307b2bca2c9b747aa2f40c5262b","tests\/bootstrap.php":"4ae74313e457f6662f4831ee2140eb34","src\/Console\/StartRetryCommand.php":"ebbff074a2e7d79f377ef520285c6109","src\/Console\/StopRetryCommand.php":"92a0540c45489bf3a6ba11c517b84699","src\/Providers\/DatabaseTransactionRetryServiceProvider.php":"a4ba3a51cad9c3a470246518bc1bf5a6","src\/Services\/TransactionRetrier.php":"504c45ab8315c7b3d60a64e0a52759af","src\/Support\/RetryToggle.php":"2516f4c290940019b1f1c069fec64be8","src\/Support\/BindingStringifier.php":"bbead2bae37761124652320cd28db412","src\/Support\/TransactionRetryLogWriter.php":"2bc60103437973d7b471ad6bc91c43cf","src\/Support\/TraceFormatter.php":"e640e32b17149f1cfec9a1d990f04c84","tests\/TestCase.php":"897cfbd81822f4b71075ccb1739df70d","tests\/Unit\/ExampleTest.php":"3bbd4ea8029698f723c35a66d8592087","src\/Providers\/DbMacroServiceProvider.php":"e17026a164c34320845575f00076938d"}}

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Resilient database transactions for Laravel applications that need to gracefully
2727
- Structured logs with request metadata, SQL, bindings, connection information, and stack traces written to dated files under `storage/logs/{Y-m-d}`.
2828
- Log titles include the exception class and codes, making it easy to see exactly what triggered the retry.
2929
- Optional transaction labels and custom log file names for easier traceability across microservices and jobs.
30+
- Convenience `DB::transactionWithRetry` macro on both the facade and individual connections so existing transaction code stays readable.
3031
- Laravel package auto-discovery; no manual service provider registration required.
3132

3233
## Installation
@@ -58,6 +59,32 @@ $order = Retry::runWithRetry(
5859

5960
`runWithRetry()` returns the value produced by your callback, just like `DB::transaction()`. If every attempt fails, the last exception is re-thrown so your calling code can continue its normal error handling.
6061

62+
### DB Macro Convenience
63+
64+
Prefer working through the database facade? Call the included `transactionWithRetry` macro and keep identical behaviour and parameters:
65+
66+
```php
67+
$invoice = DB::transactionWithRetry(
68+
function () use ($payload) {
69+
return Invoice::fromPayload($payload);
70+
},
71+
maxRetries: 5,
72+
retryDelay: 1,
73+
trxLabel: 'invoice-sync'
74+
);
75+
```
76+
77+
Need connection-specific logic? Because the macro is applied to `Illuminate\Support\Facades\DB` **and** to every resolved `Illuminate\Database\Connection`, you can call it on connection instances as well:
78+
79+
```php
80+
$report = DB::connection('analytics')->transactionWithRetry(
81+
fn () => $builder->lockForUpdate()->selectRaw('count(*) as total')->first(),
82+
trxLabel: 'analytics-rollup'
83+
);
84+
```
85+
86+
The macro is registered automatically when the service provider boots, and sets the `tx.label` container binding the same way as the helper.
87+
6188
### Parameters
6289

6390
| Parameter | Default | Description |

src/Providers/DatabaseTransactionRetryServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public function register(): void
1414
__DIR__ . '/../../config/database-transaction-retry.php',
1515
'database-transaction-retry'
1616
);
17+
18+
$this->app->register(DbMacroServiceProvider::class);
1719
}
1820

1921
/**
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace DatabaseTransactions\RetryHelper\Providers;
4+
5+
use Closure;
6+
use DatabaseTransactions\RetryHelper\Services\TransactionRetrier;
7+
use Illuminate\Database\Connection;
8+
use Illuminate\Support\Facades\DB;
9+
use Illuminate\Support\ServiceProvider;
10+
11+
class DbMacroServiceProvider extends ServiceProvider
12+
{
13+
public function register(): void
14+
{
15+
//
16+
}
17+
18+
/**
19+
* Bootstrap any package services.
20+
*/
21+
public function boot(): void
22+
{
23+
$this->registerDbFacadeMacro();
24+
}
25+
26+
protected function registerDbFacadeMacro(): void
27+
{
28+
$macro = function (
29+
Closure $callback,
30+
?int $maxRetries = null,
31+
?int $retryDelay = null,
32+
?string $logFileName = null,
33+
string $trxLabel = ''
34+
) {
35+
return TransactionRetrier::runWithRetry(
36+
$callback,
37+
$maxRetries,
38+
$retryDelay,
39+
$logFileName,
40+
$trxLabel
41+
);
42+
};
43+
44+
if (is_callable([DB::class, 'macro']) && ! DB::hasMacro('transactionWithRetry')) {
45+
DB::macro('transactionWithRetry', $macro);
46+
}
47+
48+
if (
49+
method_exists(Connection::class, 'macro')
50+
&& method_exists(Connection::class, 'hasMacro')
51+
&& ! Connection::hasMacro('transactionWithRetry')
52+
) {
53+
Connection::macro('transactionWithRetry', $macro);
54+
}
55+
}
56+
}

tests/Unit/DBTransactionRetryHelperTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ function sleep(int $seconds): void
1111

1212
use DatabaseTransactions\RetryHelper\Console\StartRetryCommand;
1313
use DatabaseTransactions\RetryHelper\Console\StopRetryCommand;
14+
use DatabaseTransactions\RetryHelper\Providers\DbMacroServiceProvider;
1415
use DatabaseTransactions\RetryHelper\Services\TransactionRetrier;
1516
use DatabaseTransactions\RetryHelper\Support\RetryToggle;
1617
use Illuminate\Container\Container;
1718
use Illuminate\Database\QueryException;
19+
use Illuminate\Support\Facades\DB;
20+
use Illuminate\Support\Traits\Macroable;
1821
use Psr\Log\AbstractLogger;
1922
use Symfony\Component\Console\Tester\CommandTester;
2023

2124
beforeEach(function (): void {
25+
FakeDatabaseManager::flushMacros();
26+
2227
$this->database = new FakeDatabaseManager();
2328
$this->logManager = new FakeLogManager();
2429

@@ -201,6 +206,26 @@ function sleep(int $seconds): void
201206
expect($record['context']['driverCode'])->toBe(1213);
202207
});
203208

209+
test('db facade macro delegates to transaction retrier', function (): void {
210+
$provider = new DbMacroServiceProvider($this->app);
211+
$provider->boot();
212+
213+
$attempts = 0;
214+
215+
expect(FakeDatabaseManager::hasMacro('transactionWithRetry'))->toBeTrue();
216+
217+
$result = DB::transactionWithRetry(function () use (&$attempts) {
218+
$attempts++;
219+
220+
return 'macro-done';
221+
}, trxLabel: 'macro-test');
222+
223+
expect($result)->toBe('macro-done');
224+
expect($attempts)->toBe(1);
225+
expect($this->database->transactionCalls)->toBe(1);
226+
expect($this->app->make('tx.label'))->toBe('macro-test');
227+
});
228+
204229
test('does not retry for non deadlock query exception', function (): void {
205230
try {
206231
TransactionRetrier::runWithRetry(function (): void {
@@ -487,6 +512,8 @@ final class CustomRetryException extends \RuntimeException
487512

488513
final class FakeDatabaseManager
489514
{
515+
use Macroable;
516+
490517
public int $transactionCalls = 0;
491518
/** @var list<array{0:string,1:array}> */
492519
public array $statementCalls = [];

0 commit comments

Comments
 (0)