Laravel can not update value in time. Simultaneously requests - php

I have PHP 8.3, and Laravel 9 project.
I have a post route for updating the balance column value. And function in controller below
public function loadFunds(FundToCardRequest $request)
{
$user = auth()->user();
$request['clientUsername'] = 'username';
$request['username'] = $user->username;
$sum = $request['amount'];
$request['amount'] *= (1 - config('commissions.credit_card_from_wallet') / 100);
$response = SomeService::post('updateBalace', $request->toArray())->collect();
if ($response->get('code') == 200) {
DB::transaction(function () use ($user, $request, $sum) {
$balance = $user->wallets()->where('currency', 'USD')->first()->pivot->balance;
$user->wallets()->updateExistingPivot(1, ['balance' => $balance - $sum]);
$user->transactions()->create([
The function receives a custom request with the following rules.
public function rules()
{
$balance_usd = auth()->user()->wallets()->where('currency', 'USD')->first()->pivot->balance;
return [
'amount' => ['numeric', 'required', new NotZeroAmount(), new SendMoneyBalance($balance_usd)],
'cardId' => ['required'],
'ArrayHashId' => ['required'],
];
}
There is a rule SendMoneyBalance that checking is the current balance enough to send amount of money.
The problem is the following. Sometimes clients can send two simultaneous requests. The first request take time for processing after which the balance should be decreased and the final amount in the balance should be not enough. But the second request passes the rules because while first balance can't update. After this two requests balance goes to a negative value.
Are there any techniques to prevent this kind of simultaneous request? Or something like await structures like in other languages.

This is called a race condition and what you basically want to do is to create some sort or unique lock per request per user or your preference.
Example
Cache::lock('foo', 10)->block(5, function () {
// Lock acquired after waiting a maximum of 5 seconds...
});
See here for ref

Related

Laravel display warning before rate limit lockout

I have a custom rate limiting rule in my RouteServiceProvider.php which looks like so;
protected function configureRateLimiting()
{
RateLimiter::for('example', function (Request $request) {
return Limit::perHour(5)->by(optional($request->user())->id ?: $request->ip())->response(function () {
return response()->view('auth.login', [
'error' =>
'You have exceeded the maximum number of login attempts. ' .
'Your account has been blocked for security reasons.',
'page' => 'login',
], 422);
});
});
}
This locks out the user after 5 attempts in an hour.
I would like to add a warning though after 2 attempts aswell, something like you have had two failed login attempts. If you continue entering an incorrect password your account will be locked.
I have tried the following in my login controller, but it doesnt work;
if (RateLimiter::remaining(optional($request->user())->id ?: $request->ip(), 2)) {
RateLimiter::hit(optional($request->user())->id ?: $request->ip());
return view('auth.login')->with([
'error' => 'You have had two failed login attempts. If you continue entering an incorrect password your account will be locked.',
'page' => 'login'
]);
}
Is this possible? I cant find anything regarding this.
Cheers,
the rate limiter information will be pass into the reponse headers X-RateLimit-Limit and X-RateLimit-Remaining which you may or may not be able to extract
It would be much easier to manually interact with RateLimiter class and manually increment the limiter, this way, you can return the remaining attempt and all the other information.
here's a basic example;
add the class use Illuminate\Support\Facades\RateLimiter;
then manually invoke hit and count the remaining attempts,
Route::get('/whatever-login-route', function( Request $request ) {
$key = 'login-limit:'.$request->ip;
//RateLimiter::resetAttempts( $key ); // resetting attempts
//RateLimiter::clear( $key ); // resetting attempts and lockout timer
return [
'hit' => RateLimiter::hit($key, 3600),
'remaining' => RateLimiter::remaining($key, 5),
'reset_at' => RateLimiter::availableIn($key)
];
});
This is just a basic example, but as you can see, in your login controller, you can pass the remaining or hit value and do your warning message after 2 hits, and return an error message with 429 header if the remaining value is less than 1 or hit value is more than 5.
Example usage in your case
$key = optional($request->user())->id ?: $request->ip();
$hit = RateLimiter::hit($key, 3600 ); // 2nd parameter is the value lockout timer in seconds
$remaining = RateLimiter::remaining($key, 5) // 2nd parameter is the number of allowed attempts in lockout define above
if ( $hit == 2 ) { // if ( $remaining == 3 )
return view('auth.login')->with([
'error' => 'You have had two failed login attempts. If you continue entering an incorrect password your account will be locked.',
'page' => 'login'
]);
}

Voting system that being run at the same time not all saved

so i am working at voting system that have code like this
public function storeVote(Request $request)
{
$voting = Voting::findOrFail($request->voting_id);
if($voting->status == 1){
$checkVote = vote::where('voting_id',$request->voting_id)->where('name',$request->name)->where('voting_candidate_id',null)->first();
if($checkVote){
\DB::beginTransaction();
try{
$candidate = candidate::findOrFail($request->voting_candidate_id);
$skor = $candidate->skor + 1;
$candidate->skor = $skor;
$candidate->update();
$checkVote->voting_candidate_id = $request->voting_candidate_id;
$checkVote->update();
$vote_ok = $voting->vote_ok + 1;
$voting->vote_ok = $vote_ok;
$voting->update();
event(new VotingEvent($skor, $voting->id, $candidate->id));
CandidateProfile::flushCache();
\DB::commit();
return response()
->json([
'saved' => true,
'message' => 'Voting done.',
]);
} catch (\Exception $e){
\DB::rollBack();
abort(500, $e->getMessage());
}
}else{
return response()
->json([
'saved' => false,
'message' => 'sorry, you already vote'
]);
}
}else{
return response()
->json([
'saved' => false,
'message' => 'Sorry, Voting session not started yet'
]);
}
}
so this function act as a way for user to vote, the participant have a unique link where they only need to choose the candidate and then it will be trigger the function above
the problem is when i tested to do like 30 vote at the same time, half of them not saved.
any idea why?
update:
the data that are not saved:
candidate skor is not updated or not multiplied
voting information about vote_ok which mean total vote that being use
Note there is a catch when you use update queries. For eg: in you above code you are updating the candicate_skor using;
$skor = $candidate->skor + 1;
$candidate->skor = $skor;
$candidate->update();
The problem arises when your server gets multiple concurrent requests for the same route. For each of the requests (let's say you have 5 requests) the function retrieves the old candidate_skore value let's say it was equal to 1. Now when each of them updates the value DB value it will be equal to 2. Even though you have 5 upvote requests that should update the DB value to 6 it updates to just 2, causing you to lose 4 votes.
Ideally, you should keep a relation table for all the votes received and only insert it into that relation table. That way even if simultaneous requests are served all of them will insert new entries to the table. Finally, your total vote should be equal to the count of all rows in that relation table.

laravel Set a time interval for sending verification code as SMS to activate the user after registration

how can I set time interval or time difference between the first time the user requested for the verification code and the second try which should be 30 seconds?
also how to display the time counter: 29:00 down to 0 seconds?
public function sendSms($request)
{
$apiKey = config('services.smsapi.ApiKey');
$client = new \GuzzleHttp\Client();
$endpoint = "https://www.sms123.net/api/send.php";
try
{
$response = $client->request('GET', $endpoint, ['query' => [
'recipients' => $request->contact_number,
'apiKey' => $apiKey,
'messageContent'=>'testSite.com verification code is '.$request->code,
]]);
$statusCode = $response->getStatusCode();
$content = $response->getBody();
$content = json_decode($response->getBody(), true);
return $content['msgCode'];
}
catch (Exception $e)
{
echo "Error: " . $e->getMessage();
}
}
Thankfully, Laravel gets you covered in this aspect. In Laravel, you can achieve rate-limiting using a middleware called throttle which comes out of the box in Laravel. You need to assign this throttle middleware to the route or group of routes.
The middleware basically accepts two parameters, specifically “number of requests” and “duration of time”, which determines the maximum number of requests that can be made in a given number of minutes.
Basic example
You can assign a throttle middleware to a single route like below
Route::get('admin/profile', function () {
//
})->middleware('auth', 'throttle:30,1');
As you can see, the above route configuration will allow an authenticated user access route 30 times per minute. If user exceed this limit within the specified time span, Laravel will return a 429 Too Many Requests with following response headers.
x-ratelimit-limit: 2
x-ratelimit-remaining: 0
x-ratelimit-reset: 1566834663
Then with vue or js on your frontend you can make a counter that will start counting the desired number so that the user knows how much time he has left.

How do I do a callback function in Laravel after a transaction processes

What I'm trying to do here is to implement a callback function in a Laravel 5.4 controller. This uses Authorize.net to process a credit card transaction, then inserts a bunch of stuff into the database, sends some messages, makes an invoice and airbill, and so on.
What I WANT to happen is:
Hit the "Submit" button, sends AJAX request
Processes the Authorize.net transaction
If good, then call a callback function to do all the gruntwork but return a transaction response.
4) Notify the user
The reason I wanna do it this way is that I want the user to wait the minimum amount of time to see the result of their payment processing without having to wait another 5 seconds or so staring at a spinning wheel waiting to go to the order complete page.
Can a callback function help me do this?
Thanks
My current implementation results in a 500 error, and I'm not quite sure what I should do from here...
[ route in web.config ]
// AJAX call to process the transaction, insert the new order, inform the user of success/failure
Route::post('/shop/processtransaction', 'OrderCheckoutController#processTransaction');
[ function processTransaction in OrderCheckoutController.php ]
public function processTransaction(Request $request) {
return self::processPaymentAndOrderInsertion($request, 'createOrder');
}
[ function processPaymentAndOrderInsertion in OrderCheckoutController.php ]
public function processPaymentAndOrderInsertion(Request $request, callable $createOrderCallback = null) {
$order_proc = new OrderProcessingTools;
$transaction_response = $order_proc->processTransaction($request);
if($transaction_response['success'] === true) {
self::$createOrderCallback($request, $transaction_response);
}
return json_encode($transaction_response);
}
[ my callback function ]
public function createOrder(Request $request, $transaction_response) {
$order_proc = new OrderProcessingTools;
$new_order = $order_proc->insertNewOrder($request);
$new_order->payment_status_id = $transaction_response['response_data']['order_payment_status_id'];
$new_order->save();
// record the payment transaction
$order_proc->insertOrderPaymentData($new_order, $transaction_response);
// insert the travelers for this order
$travelers = $order_proc->insertOrderTravelers($new_order);
// insert order inbound shipment record
$order_proc->insertInboundOrderShipping($new_order->id);
// generate inbound shipping airbill
$order_proc->generateInboundShippingAirbill($new_order->id);
/// generate the invoive
$order_proc->generateInvoice($new_order);
// send new order notification to the user
$order_proc->sendNewOrderNotificationToUser($new_order);
// send new order notification to admin
$order_proc->sendNewOrderNotificationToAdmin($new_order);
// finally kill the session variable
$_SESSION['travelers'] = [];
}
[ my previous non-asynchronous implementation looks like this...]
public function processTransaction(Request $request) {
// :: POST
// Process the Authorize.net transaction, insert the order, generate invoices
// and airbills, send notifications
$order_proc = new OrderProcessingTools;
$transaction_response = $order_proc->processTransaction($request);
if($transaction_response['success'] === true) {
// insert a new order
$new_order = $order_proc->insertNewOrder($request);
$new_order->payment_status_id = $transaction_response['response_data']['order_payment_status_id'];
$new_order->save();
// record the payment transaction
$order_proc->insertOrderPaymentData($new_order, $transaction_response);
// insert the travelers for this order
$travelers = $order_proc->insertOrderTravelers($new_order);
// insert order inbound shipment record
$order_proc->insertInboundOrderShipping($new_order->id);
// generate inbound shipping airbill
$order_proc->generateInboundShippingAirbill($new_order->id);
/// generate the invoive
$order_proc->generateInvoice($new_order);
// send new order notification to the user
$order_proc->sendNewOrderNotificationToUser($new_order);
// send new order notification to admin
$order_proc->sendNewOrderNotificationToAdmin($new_order);
// finally kill the session variable
$_SESSION['travelers'] = [];
}
// either good news or bad news at this point..
return json_encode($transaction_response);
}
When I try it this way, this is the error that is returned...
xception: "Symfony\Component\Debug\Exception\FatalThrowableError"
file: "F:\wamp64\www\uspassports\public_html\app\Http\Controllers\OrderCheckoutController.php"
line: 105
message: "Argument 2 passed to App\Http\Controllers\OrderCheckoutController::processPaymentAndOrderInsertion() must be callable or null, string given
You need to pass a callable type, but passing just the string name of the method won't work as PHP will only check if it's a global function.
You need to pass an array, with the first parameter being the object to call the method on, and the second the name of the function, like so:
return self::processPaymentAndOrderInsertion($request, [$this, 'createOrder']);
Documentation: https://www.php.net/manual/en/language.types.callable.php

Multiple request in guzzle

Here is my problem:
Spotify doesn't return all user's saved tracks. There is limit for count of returning tracks - 50 (here is API).
I found a solution that returns all user's saved track (used loop do-while). It makes a lot of requests (in my case was ~17 times - 814 tracks) But my page loads from 6 secs to 8 secs.
I read about Concurrent requests but I don't know how to use this and async requests in my situation because in my case is no known amount of requests. The loop ends only when count of returning tracks(items) are 0. Can you help me with my problem?
<?php
namespace AppBundle\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class SpotifyRequester
{
protected $client;
protected $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
$this->client = new Client();
}
public function getSavedTracks()
{
$token = $this->getToken(); // true token
$offset = 0;
do {
$response = $this->client->request('GET',
'https://api.spotify.com/v1/me/tracks?limit=50&offset=' . $offset, [
'headers' => [
'Authorization:' => 'Bearer ' . $token,
'Accept:' => 'application/json',
'Content-Type:' => 'application/json',
]
]);
// Response from current request
$content = json_decode($response->getBody()->getContents(), true);
$offset += count($content['items']);
}
while (count($content['items']) != 0);
// Count of tracks
return $offset;
}
}
Don't rely on that condition. Either rely on the next entry not being null or count the total entries you have and compare it to the total entry.
Spotify exposes a total number of entries in the pagination wrapper around the response. You can make a first request with the first 50 entries, then make concurrent requests for all remaining chunks, because you know the total number at that point.
You have to use asyncRequest() for the further requests, which returns a promise, and schedule all your remaining requests. Then you can wait for the promises sequentially using the wait() instance method. The order of your wait() calls doesn't matter, because wait() will tick the internal event loop and make progress for any of your requests. All further wait() calls take either way shorter to run or even resolve immediately.
Unfortunately, you will have to construct the URLs manually, instead of being able to rely on the next entry for your URLs.
I'd recommend to add some limit of concurrency, Spotify probably has some guidelines for that. Guzzle offers a Pool implementation for that.

Categories