├── .phpunit.result.cache ├── config └── atomic-locks-middleware.php ├── src ├── AtomicLocksMiddlewareServiceProvider.php └── AtomicLocksMiddleware.php ├── LICENSE.md ├── CHANGELOG.md ├── composer.json └── README.md /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"\/Users\/zadeveloper\/Code\/atomic-locks-middleware\/tests\/AtomicLocksMiddlewareTest.php::it":0.098}} -------------------------------------------------------------------------------- /config/atomic-locks-middleware.php: -------------------------------------------------------------------------------- 1 | 'atomic-locks-middleware', 6 | 'middleware_class' => PyaeSoneAung\AtomicLocksMiddleware\AtomicLocksMiddleware::class, 7 | 8 | 'instance' => 'AtomicLocksMiddleware', 9 | 10 | 'lock_prefix' => 'atomic_locks_middleware_', 11 | 'default_lock_duration' => 60, 12 | 13 | 'can_block' => false, 14 | 'default_block_duration' => 60, // It's generally recommended to set the block duration to be longer than the lock duration. 15 | 'block_timeout_error_message' => 'Timeout: Unable to acquire lock within the specified time.', 16 | 17 | 'message' => 'Too Many Attempts', 18 | ]; 19 | -------------------------------------------------------------------------------- /src/AtomicLocksMiddlewareServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('atomic-locks-middleware') 14 | ->hasConfigFile(); 15 | } 16 | 17 | public function boot(): void 18 | { 19 | parent::boot(); 20 | 21 | app('router')->aliasMiddleware( 22 | config('atomic-locks-middleware.middleware_name'), 23 | config('atomic-locks-middleware.middleware_class'), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) pyaesoneaung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `atomic-locks-middleware` will be documented in this file. 4 | 5 | ## v.1.3.0 - 2025-05-31 6 | 7 | ### What's Changed 8 | 9 | * Support Laravel 12 10 | 11 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.2.0...v1.3.0 12 | 13 | ## v1.2.0 - 2024-03-12 14 | 15 | - Support Laravel 11 16 | 17 | ## v1.1.1 - 2023-10-24 18 | 19 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.1.0...v1.1.1 20 | 21 | ## v1.1.0 - 2023-10-15 22 | 23 | ### What's Changed 24 | 25 | - Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/2 26 | - Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/3 27 | - Add Lock Block Feature and Additional Parameters to Middleware by @n0bitaaa in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/4 28 | 29 | ### New Contributors 30 | 31 | - @n0bitaaa made their first contribution in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/4 32 | 33 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.0.3...v1.1.0 34 | 35 | ## v1.0.3 - 2023-08-24 36 | 37 | - add $request->path() in lock prefix 38 | 39 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.0.2...v1.0.3 40 | 41 | ## v1.0.2 - 2023-08-24 42 | 43 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.0.1...v1.0.2 44 | 45 | ## v1.0.1 - 2023-08-24 46 | 47 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/compare/v1.0.0...v1.0.1 48 | 49 | ## v1.0.0 - 2023-08-24 50 | 51 | ### What's Changed 52 | 53 | - Bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.0 by @dependabot in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/1 54 | 55 | ### New Contributors 56 | 57 | - @dependabot made their first contribution in https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/pull/1 58 | 59 | **Full Changelog**: https://github.com/PyaeSoneAungRgn/atomic-locks-middleware/commits/v1.0.1 60 | -------------------------------------------------------------------------------- /src/AtomicLocksMiddleware.php: -------------------------------------------------------------------------------- 1 | $request->user()?->id ?: $request->ip(), 27 | 'ip' => $request->ip(), 28 | default => $option 29 | }; 30 | 31 | $name = "{$request->path()}_{$name}"; 32 | 33 | $lock = Cache::lock( 34 | config('atomic-locks-middleware.lock_prefix').$name, 35 | $lockDuration ?: config('atomic-locks-middleware.default_lock_duration') 36 | ); 37 | 38 | if (! $lock->get()) { 39 | if (! ($canBlock ?? config('atomic-locks-middleware.can_block'))) { 40 | return response()->json([ 41 | 'message' => config('atomic-locks-middleware.message'), 42 | ], 429); 43 | } 44 | 45 | try { 46 | $lock->block($blockDuration ?: config('atomic-locks-middleware.default_block_duration')); 47 | } catch (LockTimeoutException) { 48 | $lock->release(); 49 | 50 | return response()->json([ 51 | 'message' => config('atomic-locks-middleware.block_timeout_error_message'), 52 | ], 500); 53 | } catch (Throwable $th) { 54 | $lock->release(); 55 | 56 | return response()->json([ 57 | 'message' => $th->getMessage(), 58 | ], 500); 59 | } 60 | } 61 | 62 | app()->instance(config('atomic-locks-middleware.instance'), $lock); 63 | 64 | return $next($request); 65 | } 66 | 67 | /** 68 | * Handle tasks after the response has been sent to the browser. 69 | */ 70 | public function terminate(Request $request, Response $response): void 71 | { 72 | $instanceName = config('atomic-locks-middleware.instance'); 73 | 74 | if (app()->bound($instanceName)) { 75 | app($instanceName)->release(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyaesoneaung/atomic-locks-middleware", 3 | "description": "A package designed to ensure that only one request is processed at a time.", 4 | "keywords": [ 5 | "pyaesoneaung", 6 | "laravel", 7 | "middleware", 8 | "atomic-locks-middleware" 9 | ], 10 | "homepage": "https://github.com/pyaesoneaung/atomic-locks-middleware", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Pyae Sone Aung", 15 | "email": "pyaesoneaungrgn@gmail.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "spatie/laravel-package-tools": "^1.0", 22 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.0", 26 | "nunomaduro/collision": "^6.0", 27 | "nunomaduro/larastan": "^2.0.1", 28 | "orchestra/testbench": "^7.0|^8.0|^9.0", 29 | "pestphp/pest": "^1.21", 30 | "pestphp/pest-plugin-laravel": "^1.1", 31 | "phpstan/extension-installer": "^1.1", 32 | "phpstan/phpstan-deprecation-rules": "^1.0", 33 | "phpstan/phpstan-phpunit": "^1.0", 34 | "phpunit/phpunit": "^9.5" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "PyaeSoneAung\\AtomicLocksMiddleware\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "PyaeSoneAung\\AtomicLocksMiddleware\\Tests\\": "tests/" 44 | } 45 | }, 46 | "scripts": { 47 | "post-autoload-dump": "@composer run prepare", 48 | "clear": "@php vendor/bin/testbench package:purge-atomic-locks-middleware --ansi", 49 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 50 | "build": [ 51 | "@composer run prepare", 52 | "@php vendor/bin/testbench workbench:build --ansi" 53 | ], 54 | "start": [ 55 | "@composer run build", 56 | "@php vendor/bin/testbench serve" 57 | ], 58 | "analyse": "vendor/bin/phpstan analyse", 59 | "test": "vendor/bin/pest", 60 | "test-coverage": "vendor/bin/pest --coverage", 61 | "format": "vendor/bin/pint" 62 | }, 63 | "config": { 64 | "sort-packages": true, 65 | "allow-plugins": { 66 | "pestphp/pest-plugin": true, 67 | "phpstan/extension-installer": true 68 | } 69 | }, 70 | "extra": { 71 | "laravel": { 72 | "providers": [ 73 | "PyaeSoneAung\\AtomicLocksMiddleware\\AtomicLocksMiddlewareServiceProvider" 74 | ], 75 | "aliases": { 76 | "AtomicLocksMiddleware": "PyaeSoneAung\\AtomicLocksMiddleware\\Facades\\AtomicLocksMiddleware" 77 | } 78 | } 79 | }, 80 | "minimum-stability": "dev", 81 | "prefer-stable": true 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atomic Locks Middleware 2 | 3 | A package designed to ensure that only one request is processed at a time. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | composer require pyaesoneaung/atomic-locks-middleware 9 | ``` 10 | 11 | ## Usage 12 | 13 | By default, the atomic-locks-middleware uses `$request->user()?->id ?: $request->ip()` within atomic locks. 14 | 15 | ```php 16 | Route::post('/order', function () { 17 | // ... 18 | })->middleware('atomic-locks-middleware'); 19 | ``` 20 | 21 | If you prefer to implement IP-based locking, you can use `atomic-locks-middleware:ip`. 22 | 23 | ```php 24 | Route::post('/order', function () { 25 | // ... 26 | })->middleware('atomic-locks-middleware:ip'); 27 | ``` 28 | 29 | However, you have the flexibility to define `atomic-locks-middleware:{anything}` to customize the locking mechanism according to your preferences. 30 | 31 | ```php 32 | Route::post('/order', function () { 33 | // ... 34 | })->middleware('atomic-locks-middleware:{anything}'); 35 | ``` 36 | 37 | You can also pass additional parameters to the middleware for more customization. The available parameters are: 38 | - `{anything} (string)` : Your custom locking mechanism. 39 | - `{lockDuration} (int)` : Duration for which the lock will be held. 40 | - `{canBlock} (bool)` : Whether the request can wait for the lock or not. 41 | - `{blockDuration} (int)` : If waiting is allowed, the maximum duration to wait for the lock. 42 | 43 | > If no additional parameters are provided, the default values from the config file will be used. 44 | 45 | ```php 46 | Route::post('/order', function () { 47 | // ... 48 | })->middleware('atomic-locks-middleware:{anything}'); 49 | 50 | 51 | Route::post('/purchase', function () { 52 | // ... 53 | })->middleware('atomic-locks-middleware:{anything},60,true,60'); 54 | 55 | 56 | Route::post('/payment/process', function () { 57 | // ... 58 | })->middleware('atomic-locks-middleware:{anything},60,false'); 59 | ``` 60 | 61 | ## How Does It Work? 62 | 63 | ```php 64 | // AtomicLocksMiddleware.php 65 | 66 | public function handle(Request $request, Closure $next, string $option = null, int $lockDuration = null, string $canBlock = null, int $blockDuration = null): Response 67 | { 68 | if (! empty($canBlock)) { 69 | $canBlock = filter_var($canBlock, FILTER_VALIDATE_BOOLEAN); 70 | } 71 | 72 | $name = match ($option) { 73 | null => $request->user()?->id ?: $request->ip(), 74 | 'ip' => $request->ip(), 75 | default => $option 76 | }; 77 | 78 | $name = "{$request->path()}_{$name}"; 79 | 80 | $lock = Cache::lock( 81 | config('atomic-locks-middleware.lock_prefix') . $name, 82 | $lockDuration ?: config('atomic-locks-middleware.default_lock_duration') 83 | ); 84 | 85 | if (! $lock->get()) { 86 | if (! ($canBlock ?? config('atomic-locks-middleware.can_block'))) { 87 | return response()->json([ 88 | 'message' => config('atomic-locks-middleware.message'), 89 | ], 429); 90 | } 91 | 92 | try { 93 | $lock->block($blockDuration ?: config('atomic-locks-middleware.default_block_duration')); 94 | } catch (LockTimeoutException) { 95 | $lock->release(); 96 | 97 | return response()->json([ 98 | 'message' => config('atomic-locks-middleware.block_timeout_error_message'), 99 | ], 500); 100 | } catch (Throwable $th) { 101 | $lock->release(); 102 | 103 | return response()->json([ 104 | 'message' => $th->getMessage(), 105 | ], 500); 106 | } 107 | } 108 | 109 | app()->instance(config('atomic-locks-middleware.instance'), $lock); 110 | 111 | return $next($request); 112 | } 113 | 114 | /** 115 | * Handle tasks after the response has been sent to the browser. 116 | */ 117 | public function terminate(Request $request, Response $response): void 118 | { 119 | $instanceName = config('atomic-locks-middleware.instance'); 120 | 121 | if (app()->bound($instanceName)) { 122 | app($instanceName)->release(); 123 | } 124 | } 125 | ``` 126 | 127 | The Atomic Locks Middleware uses [Laravel Atomic Locks](https://laravel.com/docs/10.x/cache#atomic-locks) in the background. It initiates a lock at the beginning of the middleware's execution and releases the lock once the response is dispatched to the browser. 128 | 129 | ## Publish Configuration 130 | 131 | Publish the configuration for customization 132 | 133 | ```bash 134 | php artisan vendor:publish --provider="PyaeSoneAung\AtomicLocksMiddleware\AtomicLocksMiddlewareServiceProvider" 135 | ``` 136 | 137 | ```php 138 | return [ 139 | 140 | 'middleware_name' => 'atomic-locks-middleware', 141 | 'middleware_class' => PyaeSoneAung\AtomicLocksMiddleware\AtomicLocksMiddleware::class, 142 | 143 | 'instance' => 'AtomicLocksMiddleware', 144 | 145 | 'lock_prefix' => 'atomic_locks_middleware_', 146 | 'default_lock_duration' => 60, 147 | 148 | 'can_block' => false, 149 | 'default_block_duration' => 60, // It's generally recommended to set the block duration to be longer than the lock duration. 150 | 'block_timeout_error_message' => 'Timeout: Unable to acquire lock within the specified time.', 151 | 152 | 'message' => 'Too Many Attempts', 153 | ]; 154 | 155 | ``` 156 | 157 | ## Testing 158 | 159 | ```php 160 | composer test 161 | ``` 162 | --------------------------------------------------------------------------------