Laravel - Store JSON response in SQL database (via Guzzle) - php

First post! New to php & Laravel, figured I'd learn by creating a test project. I've been following Laracasts for guidance but run into issues.
What I'm looking to achieve:
Utilise Guzzle to call for an API
Store response into mySQL database
Setup a route for this
Setup a schedule so the controller runs once a day on schedule (I can start a new thread if need be)
I've got a controller setup for the Guzzle & storing data. I've got a database created which works as intended. The route I'm unsure about though on what exactly needs to be done so I'm struggling here on how to actually run the controller and store the data in the database.
I'd appreciate if anyone could review my code if I've done anything wrong, and give some guidance on routes in context of this.
Controller
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
class DataController extends Controller
{
public function index()
{
$client = new Client(['base_uri' => 'https://api.ratings.food.gov.uk/ratings']);
$response = $client->request('GET', [
'headers' => [
'x-api-version' => '2',
'Accept' => 'application/json'
]
]);
$mydata = json_decode($response->getBody()->getContents(), true);
$object = new Object();
$object->ratingId = $mydata->ratingId;
$object->ratingName = $mydata->ratingName;
$object->ratingKey = $mydata->ratingKey;
$object->ratingKeyName = $mydata->ratingKeyName;
$object->schemeTypeId = $mydata->schemeTypeId;
$object->save();
Requests::insert($object);
}
}
?>
Migration
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateRatingsTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('ratings', function (Blueprint $table) {
$table->bigIncrements('id');
$table->timestamps();
$table->integer('ratingId');
$table->string('ratingName');
$table->string('ratingKey');
$table->string('ratingKeyName');
$table->integer('schemeTypeId');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('ratings');
}
}
API JSON Response Example
{
"ratings": [
{
"ratingId": 12,
"ratingName": "5",
"ratingKey": "fhrs_5_en-gb",
"ratingKeyName": "5",
"schemeTypeId": 1,
"links": [
{
"rel": "self",
"href": "http://api.ratings.food.gov.uk/ratings/12"
}
]
},
{
"ratingId": 11,
"ratingName": "4",
"ratingKey": "fhrs_4_en-gb",
"ratingKeyName": "4",
"schemeTypeId": 1,
"links": [
{
"rel": "self",
"href": "http://api.ratings.food.gov.uk/ratings/11"
}
]
},
{
"ratingId": 10,
"ratingName": "3",
"ratingKey": "fhrs_3_en-gb",
"ratingKeyName": "3",
"schemeTypeId": 1,
"links": [
{
"rel": "self",
"href": "http://api.ratings.food.gov.uk/ratings/10"
}
]
}
}

You should leverage the power of Eloquent ORM. I've found some issues in your code. You need to remove unnecessary lines that you have written there in order to create an object. Considering that you have created the columns with same names as returned by the API response, and considering that your model name is Rating and it should be, here are my suggestions:
Your controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use GuzzleHttp\Client;
use App\Rating;
class DataController extends Controller
{
public function index()
{
$client = new Client(['base_uri' => 'https://api.ratings.food.gov.uk/ratings']);
$response = $client->request('GET', [
'headers' => [
'x-api-version' => '2',
'Accept' => 'application/json'
]
]);
$mydata = json_decode($response->getBody()->getContents(), true);
/* You don't need to create an object as you are already parsing the response as an array, so remove below lines */
// $object = new Object();
// $object->ratingId = $mydata->ratingId;
// $object->ratingName = $mydata->ratingName;
// $object->ratingKey = $mydata->ratingKey;
// $object->ratingKeyName = $mydata->ratingKeyName;
// $object->schemeTypeId = $mydata->schemeTypeId;
// $object->save();
Rating::create($mydata);
}
}
And add make the columns fillable in your Rating model by adding a protected static $fillable property to your Rating model:
protected static $fillable = ['ratingId', 'ratingName', 'ratingKeyName', 'schemeTypeId'];
If above solution isn't the one you liked, then you need to either treat $mydata as an array, i.e. do $mydata['ratingId'] to get ratingId, not $mydata->ratingId or remove true argument from json_decode() to parse the response as an object, not an array.

Related

Laravel custom validator returns page instead of json

I made a controller to store tasks, it looks like:
public function store(StoreTaskRequest $request)
{
$validated = $request->validated();
return response()->json($request);
}
For the request validation i made a custom validator, that looks like
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTaskRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'project_id' => 'required',
'project_name' => 'required',
'task_id' => 'required',
'task_name' => 'required',
'startDate' => 'required',
'endDate' => 'required',
];
}
public function messages()
{
return [
'project_id.required' => 'required!',
'project_name.required' => 'required!',
'task_id.required' => 'required!',
'task_name.required' => 'required!',
'startDate.required' => 'required!',
'endDate.required' => 'required!',
];
}
}
When i make a post request to that controller with incorrect data it returns an html page but when i post it with the correct data it returns the request as json
By the way i make my post request with: reqbin.com
Post request headers:
X-Csrf-Token: ....
Content-Type: application/json
Post Params:
{
"project_name": "Wiza",
"task_id": 1,
"task_name": "test 1",
"startDate": {
"week": 1,
"start" : 11,
"year": 2022
},
"endDate": {
"week": 1,
"start" : 11,
"year": 2022
}
}
Does anyone have any idea why its returning a html page instead of an validation error?
Edit:
api.php is empty
web.php
Route::post('api/v1/tasks/', [TaskController::class, 'store']);
//Also tried
Route::resource('api/v1/tasks/', TaskController::class);
Add this code in your custom validator.
/**
* Get the error messages for the defined validation rules.*
* #return array
*/
protected function failedValidation(ValidationValidator $validator)
{
throw new HttpResponseException(response()->json([
'message' => implode('', collect($validator->errors())->first()),
'status' => true
], 422));
}
I have faced the same problem and below mentioned way works for me.
Header Debugging
First You need to verify the list of headers exists in your request. You can do this by following the below code in your controller store method:
public function store(StoreTaskRequest $request)
{
dd($request->headers); //dump your headers exists in $request
$validated = $request->validated();
return response()->json($request);
}
and that will show you something like:
Solution:
Bestway and clean way to append those headers by using middleware i.e.
I've created ForceJsonResponse middleware.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ForceJsonResponse
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}
after that, I've registered that middleware in app/Http/Kernel.php as shown in the screenshot.
then attach that middleware with that route as shown in the screenshot:
laravel doc reference for middleware attachement
Worst case scenario
if still, it doesn't work then you can use try catch statement

Why {} is a not valid JSON and how to validate empty JSON? [duplicate]

I'm making a Laravel API, but I can't seem to send JSON data in one of the posts. I checked the other posts in StackOverflow, but it seems that my JSON request is correct, so I can't seem to find the error:
Here is the code in my Controller's method:
$validator = Validator::make($request->all(), [
"name" => "required|string",
"colors" => "json",
"sizes" => "json"
]);
if($validator->fails())
return response()->json(["errors" => $validator->errors()], 400);
Here is the request body:
{
"name": "Test",
"colors": {
"Man": "#0000ff",
"Second": "#FF0000"
},
"sizes": {
"titles": "20px"
}
}
The error:
{
"errors": {
"colors": ["The colors must be a valid JSON string."],
"text_sizes": ["The text sizes must be a valid JSON string."]
}
}
What seems to be the problem? Thank you!
Well you need to pass an JSON String instead of an JSON Object. This can be done either by json_encode or JSON.stringify.
As an answer on your last comment.:
You could either do this in your frontend application with JSON.stringify or you could implement an Form Request with an prepareForValidation(): https://laravel.com/docs/8.x/validation#prepare-input-for-validation.
Where you would do an json_encode() on the json properties. Like:
protected function prepareForValidation()
{
$this->merge([
'colors' => json_encode($this->colors),
'text_sizes' => json_encode($this->text_sizes)
]);
}
Or in your case:
$validator = Validator::make($request->merge([
'colors' => json_encode($request->colors),
'text_sizes' => json_encode($request->text_sizes)
]), [
"name" => "required|string",
"colors" => "json",
"sizes" => "json"
]);
I found a simple solution, just use double quotes to represent a json string, and escape the double quotes inside the json:
{ "name": "Test", "colors": "{\"Man\": \"#0000ff\",\"Second\": \"#FF0000\"}", "sizes": "{\"titles\": \"20px\"}" }
This solve my issue sending json string from Insomnia Api rest.
laravel have a convenient way to validate a request please follow these steps to make your own request and response for api
1-php artisan make:request YourRequest
Then customize your request like this
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Response;
class OrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(): array
{
$rules = [
'your filed name' => 'required',
];
return $rules;
}
/**
* Get the error messages that apply to the request parameters.
*
* #return array
*/
public function messages()
{
return [
'your field name' => 'custom message',
];
}
/**
* Handle a failed validation attempt.
*
* #param Validator $validator
* #return void
*
* #throws ValidationException
*/
protected function failedValidation(Validator $validator)
{
$error = collect($validator->errors())->collapse()->toArray();
$errors = implode(' | ', $error);
throw new HttpResponseException(response()->json(
['response' => ['status' => false, 'message' => $errors]],
Response::HTTP_UNPROCESSABLE_ENTITY));
}
}
this will solve error as recommended by laravel and its a professional way
then use this Request into controller .
if you want to use your own code then
just use json_encode($this->text_sizes)

How to update foreign key ide value in Laravel Eloquent?

I'm quite new to Laravel and I was not able to find the answer to this problem neither on Laravel docs, nor here.
I guess it's just a matter of how to search for it, cause I'm pretty sure it's a common case.
I have two models in relationship (this is a simplified case), I retrieve the info I need through a Resource file, but I'm not able to understand how to properly store or update info.
Here's a code example:
Models\Company.php
class Company extends Model
{
protected $fillable = [
'name', 'blablabla', 'country_id', 'blablabla2',
];
public function country() {
return $this->belongsTo(Country::class);
}
}
Models\Country.php
class Country extends Model
{
protected $fillable = [
'code', 'name', 'prefix', 'tax_code_id',
];
public function companies() {
return $this->hasMany(Company::class);
}
}
Then I have a CompanyController file to manage API requests:
Controllers\CompanyController.php
class CompanyController extends BaseController
{
public function index()
{
$companies = Company::paginate();
$response = CompanyResource::collection($companies)->response()->getData(true);
return $this->sendResponse($response, 'Companies retrieved successfully');
}
public function store(Request $request)
{
$input = $request->all();
$validator = Validator::make($input, $this->validation_rules);
if($validator->fails()){
return $this->sendError('Validation error.', $validator->errors());
}
$company = Company::create($input);
return $this->sendResponse($company->toArray(), 'Company added successfully.');
}
}
...
public function update(Request $request, Company $company)
{
$input = $request->all();
$validator = Validator::make($input, $this->validation_rules);
if($validator->fails()){
return $this->sendError('Validation error.', $validator->errors());
}
$company->update($input);
return $this->sendResponse($company->toArray(), 'Company updated successfully.');
}
And here the CompanyResource I'm using to display info as I need.
Resources/CompanyResource.php
class CompanyResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'blablabla' => $this->blablabla,
'country' => $this->country,
'blablabla2' => $this->blablabla2,
];
}
}
So when retrieving Companies (or single company) I get a nested JSON:
{
"id": "1",
"name": "something",
"blablabla": "blablabla",
"country": {
"id": "100",
"code": "MA",
"name": "Mars",
"prefix": "123",
"tax_code_id": "#$%"
},
"blablabla2": "blablabla2"
}
If I create or update a new company I send a payload that has the same structure of what I'm getting above, but if I edit country id value my company model doesn't get it.
PUT Api/companies/1
{
"name": "something",
"blablabla": "blablabla3",
"country": {
"id": "200",
"code": "JU",
"name": "Jupiter",
"prefix": "456",
"tax_code_id": "#=%"
},
"blablabla2": "blablabla2"
}
I'm expecting to update country_id field in companies table for record 1 so that it matches payload (so going from 100 to 200), but it's not happening.
I could edit frontend logic in order to send only country_id in payload since I'm not going to update countries table and all that additional info is redundant, but I'd like to know how to manage it in controller with Laravel.
Would you mind helping me? Thanks in advance.
If you want it to work with the code now, you need to have country_id in the root JSON object you are sending. As this is the way you would fill the id. This is not the best approach in my opinion, but this is why your update is not working at the moment.
{
"name": "something",
"blablabla": "blablabla3",
"country_id": 200,
...
I actually like the approach of sending complete objects. Commonly to fill id's is not good, as it can interfere with the way relations work. Laravel will set your relationships when you associate, if not you are not guaranteed to have the correct relationship after the fill.
Therefor i would fetch out the id and associate the country object with the company. In a logic similar to this.
// only fill non relation fields, fill used as save is needed after associate()
$company->fill($request->only(['name', 'blabla']));
$company->country()->associate(Country::find($request->get('country')['id']));
//associate does not save
$company->save();
I wrote a gist for this years ago that can relate any two models regardless of their relationship type. You just need to supply it with the name of the relationship method: https://gist.github.com/kmuenkel/055f107139d904e30810bf53750d9c6e

Laravel how use value instead of full object with join in eloquent model

When I call eloquent:
$user = User::where('idUser', 1)->with(['privilege'])->first()->toArray();
It gives me:
{
"idUser": 1,
"name": "UserName",
"email": "UserName#gmail.com",
"image": "https://image.com",
"createdAt": "2019-05-07 15:43:47",
"privilege": {
"idPrivilege": 1,
"name": "user"
}
}
When I call Eloquent:
$user = User::where('idUser', 1)->with(['privilege:name'])->first()->toArray();
Element privilege in json is set to null, but when I call:
$user = User::where('idUser', 1)->with(['privilege:idPrivilege,name'])->first()->toArray();
It is as same as first call. How can I set element privilege to f.e. user (I just want a simple value instead of the full object of Privilege)?
I can use something like:
$user['privilege'] = $user['privilege']['name'];
But this one does not look so nice!
Using resource:
public function toArray($request)
{
return [
'idUser' => $this->idUser,
'name' => $this->name,
'email' => $this->email,
'privilege' => $this->privilege['name'],
'createdAt' => $this->created_at,
];
}
In controller:
$user = User::where('idUser', 1)->with('privilege')->first();
return UserResource::make($user);
Gives:
{
"data": {
"idUser": 1,
"name": "UserName",
"email": "UserName#gmail.com",
"privilege": "user",
"createdAt": "2019-05-07 15:43:47"
}
}
How can i just return object instead of data{object} ?
Try without the backets:
$user = User::where('idUser', $id)->with('privilege:name')->first()->toArray();
or this:
$user = User
::where('idUser', $id)
->with(['privilege' => function($query) {
return $query->select('name');
}])
->first()
->toArray();
But then, you could customize the response to return to your view using API Resources. With this, you can have many different resources to use on the same elements and format the response to any of your needs.
As stated in HCK's answer, you could use
$user = User
::where('idUser', $id)
->with(['privilege' => function($query) {
return $query->select('name');
}])
->first()
->toArray();
To get what you need. Now, if you are already using API Resources, and want to remove the outter data object, you can add the following in your AppServiceProvider boot method:
use Illuminate\Http\Resources\Json\Resource;
class AppServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Resource::withoutWrapping(); // With this, your resources won't have the
// outter data wrapping
}
}
Just take a look at the docs!

Laravel parse request into Eloquent model with relations

Im developing a Laravel API that will host and manage the data for a mobile application. The application that I am writing will make AJAX requests and send over JSON data to the Laravel controller.
This works fine for a basic model however I am unable to get this working for nested models. Ill explain:
So I have the following model structure:
Shelf ---- Has Many ----> Boxes ---- Has Many ----> Products
Shelf:
class Shelf extends Model
{
protected $fillable = ['name', 'location'];
public function boxes()
{
return $this->hasMany('App\Box');
}
}
Box:
class Box extends Model
{
protected $fillable = ['name', 'size', 'label'];
public function shelf()
{
return $this->belongsTo('App\Shelf');
}
public function products()
{
return $this->hasMany('App\Product');
}
}
Product:
class Product extends Model
{
protected $fillable = ['name', 'price', 'quantity'];
public function box()
{
return $this->belongsTo('App\Box');
}
}
All my models have validation checking before anything is saved.
I send the following request to my Laravel controller:
{
"name":"Shelf 1",
"location":"LOC1"
"boxes":[{
"name":"box 1",
"size": 130,
"label": "B1",
"products":[
{"name":"Prod1","price":23.00,"quantity":5},
{"name":"Prod2","price":13.00,"quantity":2}
]
}, {
"name":"box 2",
"size": 130,
"label": "B2",
"products":[
{"name":"Prod3","price":3.00,"quantity":15},
{"name":"Prod4","price":7.00,"quantity":8}
]
}, {
"name":"box 3",
"size": 160,
"label": "B3",
"products":[
{"name":"Prod5","price":103.00,"quantity":9},
{"name":"Prod6","price":83.00,"quantity":1}
]
}]
}
When I receive the above data in my Laravel controller i use:
$shelf = new Shelf;
$shelf->fill($request->all());
$shelf->save();
To get all of the data however this will only save the Shelf and not any of the relationships. Is there a common way (or library) I can use to parse the JSON within the Laravel controller?
As far as I'm aware Eloquent doesn't offer anything like that. You would need to save each one individually. May I suggest something like this:
$shelf = Shelf::create($request->only(['name', 'location']));
foreach ( $request->input('boxes') as $box )
{
$box = new Box($box);
$shelf->boxes()->save($box);
$pTemp = [];
foreach ( $box['products'] as $product )
{
$pTemp[] = new Product($product);
}
$box->products()->saveMany($pTemp);
}
Update
To minimise the number of queries run we can loop through all the boxes once, create them then loop through them again to create all the products. You're still having to run one query per box to create the products, but as far as I can see there is no way around that.
$shelf = Shelf::create($request->only(['name', 'location']));
$bTemp = [];
foreach ( $request->input('boxes') as $i => $box )
{
$bTemp[$i] = new Box($box);
}
$shelf->boxes()->saveMany($bTemp);
foreach ( $request->input('boxes') as $i => $box )
{
$pTemp = [];
foreach ( $box['products'] as $product )
{
$pTemp[] = new Product($product);
}
$bTemp[$i]->products()->saveMany($pTemp);
}

Categories