Lumen : 1s, Laravel : 5s for a simple api call? - laravel

So I started a project not long ago for an API on Laravel, and I thought why not give Lumen a shot. But in the end, I want to use Sanctum, Socialite, etc... And I read pretty much everywhere that the performance difference is not that big nowadays anyway.
So I migrated my code from Lumen to Laravel, and after a few tweaks, everything works as before... Except that now a very simple API call takes 5s. Granted, it might be my setup - wsl2 isn't particularly fast. But still, the same call in Lumen was taking ~1000ms.
Route::post('register', [AuthController::class, 'register']);
Controller:
public function register(Request $request): JsonResponse {
$this->validate($request, [
'phone' => 'required|string|phone',
'phone_country' => 'required_with:phone',
]);
$phone = phone($request->get('phone'), [$request->get('phone_country')]);
try {
$user = User::createByPhone($phone);
return response()->json(['user' => $user->id, 'message' => 'SMS_SENT'], 201);
} catch (\Exception $e) {
return response()->json(['message' => 'User Registration Failed - ', 'error' => $e], 409);
}
}
Function in model:
public static function createByPhone($phone) {
return DB::transaction(function () use ($phone) {
$user = User::create();
$user->phoneNumbers()->create([
'did' => $phone
]);
return $user;
});
}
So, pretty simple stuff. Now, why is that taking so long? ~6000ms. Am I missing something?
(On a more general note, is there a way to cut from Laravel things that aren't needed for an API only?)
Thanks ahead.

I don't see anything really wrong with your code. I guess this has something to do with the speed wsl2 can read files. We had issues with windows machines and Laravel in Docker. We added Swoole to our project and this helped alot on WSL2.
Laravel now has a first party package called Octane to add Swoole to your project. You can try and install that to see if it helps.

Related

Laravel Livewire The POST method is not supported for this route

As soon as I inserted this title the system showed me all the similar questions, and none of them help me. I get this error "The POST method is not supported for this route." no matter what I try. Even worse, is I already made another component with the identical logic, and that one works good.
here are the routes: (teeoffform works, bulletin does not)
Route::get('/bulletin', function () {
return view('bulletin');
});
Route::get('/teeoffform', function () {
return view('teeoffform');
});
Here are the form tags: both identical one works one doesn't
<form wire:submit.prevent="submit" method="POST">
this is my component from the one that doesn't work (bulletin)
the only difference from the other one that does work, is that there is no rendering method, so I tried to take it out and see if that was the problem, but no luck... I thought, since my route is alrady calling a view maybe the conflict is there... but it doesn't matter, I get the error anyways, and I'm out of ideas.
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Bulletins;
use App\Models\User;
class Bulletin extends Component
{
public $title;
public $message;
public $messagesending;
public $user_email;
public $userTable_email;
public $expires;
public $success_message;
protected $rules = [
'title' => 'required',
'message' => 'required',
'user_email' => 'required',
'expires' => 'required',
];
public function render()
{
return view('livewire.bulletin', ['email_data' => User::orderBy('email','asc')->get()]);
}
public function submit()
{
$this->validate();
$sendMessage = new Bulletins;
$sendMessage->title = $this->title;
$sendMessage->message = $this->messagesending;
$sendMessage->user_email = $this->user_email;
$sendMessage->expires = $this->expires;
$sendMessage->save();
$this->success_message = 'Message Sent Successfully';
}
}
I really don't get it... I looked for 4 hours now why this is happening.
I ran into this issue as well and found that I was not including the Livewire styles and scripts in my apps layouts blade files.
#livewireStyles
#livewireScripts
I found the difference, not in the logic of the code, but where I was running it from. If I was testing from (localhost/bulletin) I was getting that error. if I included the component inside the dashboard (localhost/home) and ran it from there, then everything worked...
why is that? I can't go to (localhost/bulletin) without being logged in, so I was logged in.

Lumen job dispatching done without database Queue Driver

What do I have:
Lumen service which processing particular Job
Laravel portal which sending file to that service for processing by it
Once it was using only JS and Ajax it worked almost fine - the only what I had to implement is CORS middleware. However after I moved logic to JWT (using jwt-auth package) and GuzzleHttp (I'm using it to send requests to service API) Job stopped processing throught database queue instead it running as if Queue driver being set to sync.
Following is controller which I'm calling during API call:
public function processPackageById(Request $request) {
$id = $request->package_id;
$package = FilePackage::where('id', '=', $id)->where('package_status_id', '=', 1)->first();
if($package) {
Queue::push(new PackageProcessingJob(
$this->firm,
$this->accounts,
$package
));
return 'dispatching done for ' . $id;
}
return 'dispatching not done for ' . $id;
}
where $this->firm and $this->accounts are injected Repositories for particular models. FilePackage object being created on Laravel site and both shares same database to work with.
As result no job being incerted into jobs table. When I use Postman everything is fine. However when I'm trying to send request from Laravel backend:
public function uploaderPost(Request $request)
{
// Here we get auth token and put into protected valiable `$this->token`
$this->authorizeApi();
$requestData = $request->except('_token');
$package = $requestData['file'];
$uploadPackageRequest =
$this->client->request('POST', config('bulk_api.url') .'/api/bulk/upload?token=' . $this->token,
[
'multipart' => [
[
'name' => 'file',
'contents' => fopen($package->getPathName(), 'r'),
'filename' => $package->getClientOriginalName(),
],
]
]);
$uploadPackageRequestJson = json_decode($uploadPackageRequest->getBody()->getContents());
$uploadPackageRequestStatus = $uploadPackageRequestJson->status;
if($uploadPackageRequestStatus == 1) {
$package = BulkUploadPackage::where('id', '=',$uploadPackageRequestJson->id)->first();
// If package is okay - running it
if($package !== null){
// Here where I expect job to be dispatched (code above)
$runPackageRequest =
$this->client->request('POST', config('api.url') .'/api/bulk/run?token=' . $this->token,
[
'multipart' => [
[
'name' => 'package_id',
'contents' => $package->id
],
]
]);
// Here I'm receiving stream for some reason
dd($runPackageRequest->getBody());
if($runPackageRequest->getStatusCode()==200){
return redirect(url('/success'));
}
}
}
return back();
}
Could anyone advise me what is wrong here and what causes the issue?
Thank you!
Alright, it was really interesting. After echoing config('queue.default') in my contoller it appeared that it's value indeed sync nevertheless that I set everything correctly.
Then I assumed that maybe the reason in Laravel itself and its variables. Indeed in .env file from Laravel side QUEUE_DRIVER being set to sync. After I changed it to QUEUE_DRIVER=database everything started working as expected.
Hope that will help someone in future.

How To Test Artisan Commands with ask() in Laravel 5.4

Trying to write a test for laravel php artisan command with ask() function. I have never used mockery before, but when i try to run test, it freezes, so i guess, i'm doing something wrong.
MyCommand.php:
public function handle()
{
$input['answer1'] = $this->ask('Ask question 1');
$input['answer2'] = $this->ask('Ask question 2');
$input['answer3'] = $this->ask('Ask question 3');
//--- processing validation
$validator = Validator::make($input, [
'answer1' => 'required',
'answer2' => 'required',
'answer3' => 'required',
]);
if ($validator->fails()) {
// processing error
}
} else {
// saving to DB
}
}
And my unit test:
$command = m::mock('\App\Console\Commands\Questions');
$command->shouldReceive('ask')
->andReturn('Answer 1')
->shouldReceive('ask')
->andReturn('Answer 2')
->shouldReceive('ask')
->andReturn('Answer 3')
$this->artisan('myCommand:toRun');
$this->assertDatabaseHas('myTable', [
'question1' => 'answer1'
]);
Laravel 5.4 - 5.6
The actual issue here is that running the console command is waiting for user input, however we are running this through PHPUnit so we are unable to enter anything.
Bumping up against limitations in unit testing can initially be frustrating, however limitations you find can end up being a blessing in disguise.
Currently, your implementation is tightly coupled to a view (a console command, so a view to an admin, but still a view none-the-less.) What could be done here is place any logic within a separate class which MyCommand can utilize, and which PHPUnit can actually test on their own. We know that the fundamentals of running a custom command work, as demonstrated in Laravel unit tests, so we can offload our logic in a separate, testable class.
Your new class might look something like this:
class CommandLogic
{
public function getQuestion1Text()
{
return 'Ask question 1';
}
public function getQuestion2Text()
{
return 'Ask question 2';
}
public function getQuestion3Text()
{
return 'Ask question 3';
}
public function submit(array $input)
{
$validator = \Illuminate\Support\Facades\Validator::make($input, [
'answer1' => 'required',
'answer2' => 'required',
'answer3' => 'required',
]);
if ($validator->fails()) {
// processing error
} else {
// saving to DB
}
}
}
...your actual unit test, something like this:
$commandLogic = new CommandLogic();
$sampleInput = [
'answer1' => 'test1',
'answer2' => 'test2',
'answer3' => 'test3',
];
$commandLogic->submit($sampleInput);
$this->assertDatabaseHas('myTable', [
'question1' => 'test1'
]);
...and your console command, something like this:
public function handle()
{
$commandLogic = new CommandLogic();
$input['answer1'] = $this->ask($commandLogic->getQuestion1Text());
$input['answer2'] = $this->ask($commandLogic->getQuestion2Text());
$input['answer3'] = $this->ask($commandLogic->getQuestion3Text());
$commandLogic->submit($input);
}
This enforces the single responsibility principle and separates the moving pieces in your codebase. I know this may feel like a bit of a cop out, but testing this stuff in Laravel 5.4 is tough. If you are willing to upgrade to 5.7 or higher, read below...
Laravel 5.7+
Laravel 5.7 introduced being able to run console tests, which satisfies the exact requirement this question is asking - https://laravel.com/docs/5.7/console-tests. This is more of a full integration test rather than a unit test.

Unable to send emails in laravel 5 app

Hello I have a laravel 5 app which is working perfectly in local environment. But in production emails are not getting sent instead I get the exception below:
1/1 FatalErrorException in AstAnalyzer.php line 125:
Cannot instantiate interface PhpParser\Parser
Path to file: /vendor/jeremeamia/SuperClosure/src/Analyzer/AstAnalyzer.php line 125
I don't get it because right now I am testing same function in local and is working. Every other path of the app is working except this one.
Below is the function:
public function update_password(Request $request, $id)
{
$this->validate($request, [
'new_password' => 'required|confirmed|min:6',
'new_password_confirmation' => 'required',
]);
$user = $this->user->get_user_by_id($id);
$password = $request->get('new_password');
$this->user->save_password($password, $id);
// Send an email informing user that we have updated his password.
Mail::queue('emails.password_update', ['user' => $user, 'password' => $password], function($message) use ($user){
$message->to($user->email, $user->name)->subject('Account Password Updated');
});
$target_location = 'users/'. $id. '/profile';
flash()->success('Password Updated Successfully');
return redirect($target_location);
}
I finally solved my problem. I ran a composer update which installed the lastest version of nikic/php-parser and equally the latest version jeremeamia/superclosure, but somehow, the class Parser which was formally use in nickic's package was now an Interface. class Multiple was now implementing the interface. So in AstAnalyzer.php in jeremeamia's package, that change was not made and instead use PhpParser\Parser as CodeParser; was used which is logical as an interface cannot be instantiated unless some binding is done. So as a quick fix, I used the previous version of nikic/php-parser.

L4.2 Event: know where event got fired from?

I have a question regarding Events with Laravel 4.2...
I currently have an event listener on "auth.login"... some code lines are executed when user logins on web version... however I would like to execute a different action if the user logged via the API controller, example: ApiController#postLogin (my mobile version).
Code in my home controller:
if (Auth::attempt(['email' => Input::get('login'), 'password' => Input::get('password')]) OR Auth::attempt(['username' => Input::get('login'), 'password' => Input::get('password')]))
{
return Redirect::intended(URL::route('dashboard.index'));
}
else
{
return Redirect::action('HomeController#getIndex')->with('poplogin', true)->with('badcredentials',true)->withInput();
}
Code in global.php (event listener)
Event::listen('auth.login', function($user)
{
//Put Login_attemp in Database for Last activity, etc
$user->login_attemp()->create(['login_ip'=>$_SERVER['REMOTE_ADDR'],'login_time'=> date('Y-m-d H:i:s',time())]);
$user->last_logged = date('Y-m-d H:i:s',time());
$user->save();
Session::flash('justlogged',true);
//other code that I didnt include..........
});
Code in my ApiController
public function getRefreshData() {
//check the token
$token = Input::get('token');
$username = Input::get('username');
$user = User::where('api_token', $token)
->where('username', $username)
->first();
if(!$user || !$token) {
return Response::json([
'error' => true,
'message' => 'Invalid Token, please re login',
'code' => 401],
401
);
}
Auth::login($user);
//5 last Timesheets + tslines, for pre-load at log-in in phone memory
//Not inserting possible creation dates between, to keep phone app 100% independent
$timesheets = $user->timesheets()->orderBy('startdate', 'DESC')->take(10)->with('tslines')->get();
//Other code that I didnt include
);
return $response;
}
I cannot control the execution of the event "auth.login" myself.. firing it manually with parameter would just double-fire the event (i think?)
Is there a way to detect where the event got fired from in the Event:listen and do not insert a "log-in attemp" (my code in event listener) each time I use the getRefreshData() function in my API? Yes, I need to log the user in my API function (for other code that isn't included)
Edit: It seems to me that the most straightforward way to handle this is to check for the token in the Event listener.
Event::listen('auth.login', function($user)
{
if (Input::has('token') && Input::has('username')) {
//Put Login_attemp in Database for Last activity, etc
$user->login_attemp()->create(['login_ip'=>$_SERVER['REMOTE_ADDR'],'login_time'=> date('Y-m-d H:i:s',time())]);
$user->last_logged = date('Y-m-d H:i:s',time());
$user->save();
Session::flash('justlogged',true);
//other code that I didnt include..........
}
});
I really would suggest, long term, looking at using the functionality demonstrated in the docs under Accessing the Logged In User, it's just going to make life easier.
Original response: It might be helpful if you posted more code, because I feel like maybe this is an instance where if we zoom out a little bit maybe there is a better way to deal with this situation. Possibly you need multiple actions, different listeners, etc.
For solving this issue though, it's easy, just pass in whatever additional data you need to via a parameter:
$response = Event::fire('auth.login', array($user, 'source' => 'ApiController#postLogin', 'mobile' => true));
Then you can set those parameters to the $event object that is passed to your listener.
Let me know if you have any further questions!
After some research, I found how I could 'bypass' the execution of the event listener when the event is fired from the ApiController, using the Request::is() function
From L4.2 Docs: http://laravel.com/docs/4.2/requests#request-information )..
My routes.php file is like so:
Route::controller('api/v1', 'ApiV1Controller');
And in my global.php (where I declare my event listener)
Event::listen('auth.login', function($user)
{
if (!Request::is('api/*'))
{
//Code that is always executed at firing of event, except when from my API controllers
//Put Login_attemp in Database for Last activity, etc
$user->login_attemp()->create(['login_ip'=>$_SERVER['REMOTE_ADDR'],'login_time'=> date('Y-m-d H:i:s',time())]);
$user->last_logged = date('Y-m-d H:i:s',time());
$user->save();
}
}

Resources