We are moving an older PHP project over to laravel. We are trying to post JSON to our api we created, but are not aware how to have the JSON be bound to a model. We added the model as a parameter to the function, it is created but none of the properties are set on it from the JSON. Does this type of model binding exist in laravel?
class CalculatorModel
{
/**
* Value A.
*
* #var integer
*/
public $A;
/**
* Value B.
*
* #var integer
*/
public $B;
}
class CalculatorController
{
// What is trying to be achieved.
public function add(CalculatorModel $model)
{
return Calculator::Add($model);
}
// What we are trying to avoid
// as there is a lot of properties/objects in our real world JSON
public function add(Request $request)
{
$a = $request->json()->all();
$m = new CalculatorModel();
$m->A = $a['A'];
$m->B = $a['B'];
....
return Calculator::Add($m);
}
}
// in reoutes/api.php
Route::post('add', 'API\CalculatorController#add');
// External library's class
class Calculator
{
public static function Add(CalculatorModel $m)
{
return $m->A + $m->B;
}
}
Simple JSON post
{
"A": 2,
"B": 2
}
In ASP.Net, we are able to add a [FromBody] attribute to the parameter so that ASP.Net would bind the content body as the model instead of form content. We are looking for similar functionality in laravel.
There is no such binding in Laravel. Laravel binding is about Models/DB access as #PKeidel said. All you need is controller method without any models.
public function add(Request $request)
{
return $request->A + $request->B;
}
UPD: What about new constructor for CalculatorModel class?
public function __construct(array $properties = [])
{
foreach ($properties as $key => $value) {
$this->{$key} = $value;
}
}
public function add(Request $request)
{
$m = new CalculatorModel($request->all());
return Calculator::Add($m);
}
In any case Laravel does not offer out of the box solution for this.
Try this to wrap your API, which can then be used by Eloquent as if it were a database model:
https://github.com/CristalTeam/php-api-wrapper
All what models are about is saving/reading something from the database. So if this is not what you want, forget about it. Because in Laravel models are bound to database tables ;-)
If you just want so receive some values as json, do a calculation and return a value you are thinking to complicated.
Create a route:
Route::post('add', function() {
$data = request()->json();
return $data->get('A') + $data->get('B');
});
Or:
Route::post('add', function(\Illuminate\Http\Request $r) {
return $r->A + $r->B;
});
This is all it takes.
After that just make sure to send your data with json header. Like so:
fetch('/add', {
method:"post",
headers: {'Content-Type': 'application/json'},
body: '{"A": 2,"B": 2}'
})
.then((d) => d.text())
.then((html) => {
output.innerHTML = html;
})
See it in action here: https://laravelplayground.com/#/snippets/006b4871-5d92-4a2d-b8af-8a21423024e6
Related
Let's consider the following example: a thread has posts, and the posts also have a "thread" relation. The title of each post must include the title of the parent thread.
class Thread extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function getTitleAttribute(string $title): string
{
return $this->thread->title . ': ' . $title;
}
}
What I want to achieve:
//when we load the posts using the thread...
$posts = $thread->posts;
//...I want the "thread" relation of each post to be automatically set to $thread, so that:
$posts->first()->thread === $thread //true
By default it's not true. And if we do this:
$array = $thread->posts->toArray();
this will cause loading of the thread for each post one by one from DB which is super non-optimal. Is there some elegant Laravel technique to setup relations of the just loaded models?
You can lazy load them like this
$posts = $thread->posts()->with('thread')->get();
If you dont want the extra query, you can use map()
$thread->posts->map(function($post) use ($thread) {
return $post->setRelation('thread', $thread);
});
This will lead to the same amount of object but will also lead to loop of references.
//this is defined and doesn't use more object or launch other queries
$thread->posts->first()->thread->posts()->first()->thread;
if you want to Automate it, I suggest you create a function on Thread model to get the posts threaded.
public function loadThreadedPosts()
{
$this->posts->map(function($post) {
return $post->setRelation('thread', $this);
});
return $this;
}
//then you can
$thread->loadThreadedPosts()->posts;
If you want it to automatically be done when you specifically call for the relation "posts" on the Thread::class model, add this method to your Thread::class to overwrite the function present in the Trait HasAttributes at your own risk
/**
* Get a relationship value from a method.
*
* #param string $method
* #return mixed
*
* #throws \LogicException
*/
protected function getRelationshipFromMethod($method)
{
$relation = $this->$method();
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.', static::class, $method
));
}
return tap($relation->getResults(), function ($results) use ($method) {
if ($method == "posts") {
$results->map(function($post) {
return $post->setRelation('thread', $this);
});
}
$this->setRelation($method, $results);
});
}
Hope you understand that this overwrites a vendor method and might lead to future issues, also I dont think that this one method works with eager loading (for example: Thread::with('posts')->get()) and I dont know what else might get broken/have unexpected behavior.
As I said, at your own risk (bet/hope ->loadThreadedPosts() looks more interesting now)
**I have a problem with passing parameters on export file, I want to filter the export according to date selected on page. Hope you help me with this issue. Thanks **
This is my ExportController, I request data from form to my controller to give the export collection a date.
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
use App\Exports\ExportAttendance;
use Maatwebsite\Excel\Facades\Excel;
class ExportController extends Controller
{
public function export(Request $request)
{
return Excel::download(new ExportAttendance($request->input('min'),$request->input('max')),'Attendance.xlsx');
}
}
This is my ExportAttendance.php, this is responsible for the exportation of collections. On the query function I want to filter the data according on the date requested on the controller. How could I passed a data from controller to my Export.php, I did used constructors but it always return errors.
public function headings():array{
return[
'Name',
'In (AM)',
'Out (AM)',
'In (PM)',
'Out (PM)',
'Meeting',
'Task',
'Note',
'Total Hours',
'Date'
];
}
public function query()
{
$start = "2021-06-14";
$end = "2021-06-14";
return Attendance::select('Name','InAm','OutAM','InPM','OutPM','Meeting','SpecialTask','Undertime','TotalHours','Date')->whereBetween('Date',[$start,$end]);
}
public function collection()
{
return Attendance::all();
}
public function map($attendance):array
{
return[
$attendance->Name,
$attendance->InAM,
$attendance->OutAM,
$attendance->InPM,
$attendance->OutPM,
$attendance->Meeting,
$attendance->SpecialTask,
$attendance->Undertime,
$attendance->TotalHours,
$attendance->Date,
];
}
Instead of passing the $request object to the export class. You can simply use the request helper method.
public function query()
{
return Attendance::select('Name','InAm','OutAM','InPM','OutPM','Meeting','SpecialTask','Undertime','TotalHours','Date')
->whereBetween('Date',[request('start'), request('end')]);
}
Passing parameters is supposed to work too. Pls let me know what errors you see so I can update my answer.
I used Constructor
public function query()
{
$start = "2021-06-14";
$end = "2021-06-14";
return Attendance::select('Name','InAm','OutAM','InPM','OutPM','Meeting','SpecialTask','Undertime','TotalHours','Date')->where('Date','=',$this->year);
}
And this is my controller,
public function export(Request $request)
{
ob_start();
$datestart = $request->input('min');
$datestart = ob_get_contents();
return Excel::download(new ExportAttendance($datestart),'Attendance.xlsx');
ob_end_flush();
}
Another problem arise, when I used ob_end_clean() all my variable values returned nulls.
My Controller File
public function enquiryExport($id, Request $request)
{
$id[] = $request->explode(",",$id);
return Excel::download(new EnquiryExport($id), 'enquiry.xlsx');
}
and My Export File
protected $id;
function __construct($id) {
$this->id = $id;
}
public function collection()
{
return Enquiry::whereIn('id',explode(",",$this->id))->get();
/* return Enquiry::all(); */
}
Route is like
Route::get('enquiryExport', 'enquiryController#enquiryExport');
Still I am getting this error
"message": "Too few arguments to function App\\Http\\Controllers\\enquiryController::enquiryExport(), 1 passed and exactly 2 expected",
I am checkbox id through AJAX here.
The problems is your Route method.
Get method: the query string (name/value pairs) is sent in the URL of a GET request
Post method: the data sent to the server with POST is stored in the request body of the HTTP request
If you use Get method: try this (I have just read it, not tried)
Route::get('enquiryExport/{id}', 'enquiryController#enquiryExport')->name('enquiryExport');
Submit
If you use Post method: try this (I am used to use this)
Route::post('enquiryExport', 'enquiryController#enquiryExport');
public function enquiryExport(Request $request)
{
return Excel::download(new EnquiryExport($request->input('id')), 'enquiry.xlsx');
}
You can read more here: https://www.w3schools.com/tags/ref_httpmethods.asp
Try this
In controller:
public function enquiryExport(Request $request, $id)
{
return Excel::download(new EnquiryExport($request->id), ''.date('Y-m-d'). '.xlsx', \Maatwebsite\Excel\Excel::XLSX);
}
In Export File:
protected $id;
function __construct($id) {
$this->id = $id;
}
public function collection()
{
return Enquiry::where('id', $this->id)->get();
}
public function map($enquiry): array
{
return [
// $enquiry->WRITE YOUR RECORDS,
// ...
];
}
public function headings(): array
{
return [
//NAME HEADINGS(TITLE) OF YOUR RECORDS IN SIDE SINGLE QUOTATION,
// ...
];
}
In Route:
Route::get('enquiryExport/{id}', 'enquiryController#enquiryExport');
To transform a database entity to an API response Laravel support resources, eg. UserResource extends JsonResource. The resource allows me to cleanly define which fields from the entity should be included in the response, how to transform them etc.
Is there a similar functionality for requests? My requests typically look like this:
public function create(JsonRequest $request): UserResource
{
$data = $request->json()->all();
/* Remove, transform, add request fields etc. */
$user = User::create($data);
$user->save();
return new UserResource($user);
}
In our case we have a legacy database behind a modern API so there are a number of fields that need to transformed, renamed etc. before pushing them into the entity class. The fields differ from request to request but the steps are very similar. Is there a less boilerplate-y way to do this, something similar to how resources transform entities to responses?
Something like:
class UserRequest extends JsonRequest {
public function fromArray(JsonRequest $request) {
…
}
}
Then the request could look like this:
public function create(UserRequest $request): UserResource
{
$user = User::create($request);
$user->save();
return new UserResource($user);
}
I suppose, that most of your problems can solve form request. See example below
Form request class:
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
class TestRequest 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 [
'date' => 'required|date_format:Y-m-d H:i:s',
'name' => 'required|string',
];
}
// here you can specify custom error messages
public function messages()
{
return [
'date.required' => 'No date specified',
'date.date_format' => 'Invalid date format',
'name.required' => 'No name specified',
'name.string' => 'Invalid name format',
];
}
// here you can implement some data mapping before validation
protected function validationData()
{
return $this->transform($this->all());
}
// some data transformation logic
// You can place it anywhere in your applciation services
protected function transform($input)
{
$transformed = [];
foreach ($input as $field => $value) {
if ($field == 'name') {
$value = strtoupper($value);
} elseif ($field == 'date') {
$value = Carbon::parse($value)->toDateTimeString();
}
$transformed[$field] = $value;
}
return $transformed;
}
public function failedValidation(Validator $validator)
{
// here you can implement custom validation failure
parent::failedValidation($validator);
}
}
Here is my test route: Route::get('/test', 'TestController#index');
And controller:
use App\Http\Requests\TestRequest;
class TestController extends Controller
{
public function index(TestRequest $request)
{
return response()->json($request->validated());
}
}
So, then requesting route: curl -H 'Accept: application/json' 'http://localhost:8000/test?date=01.01.2019&name=petya'
And getting response: {"date":"2019-01-01 00:00:00","name":"PETYA"}
And dont be shy to see source code of request and form request, cause of not all methods you wish are described in docs. Hope this helps
I am trying to sanitize the user input in my application following this article
Below is my request
class TestRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
$this->sanitize();
return [
'title'=>'required|max:100'
];
}
public function sanitize()
{
$input = $this->all();
if(!empty($input))
{
$input['title'] = trim(strip_tags($input['title']));
$this->replace($input);
}
}
}
Tough the title is required field, if I try to put <h1></h1> as input in the title field, as per the logic in sanitize() function the tags are stripped away, but an empty string is saved in the database. The required field validation in the rules in not taking any effect.
How to handle this?
Update:
Below is the controller method for saving the request.
public function save(TestRequest $request)
{
$input = $request->all();
...
}
First option is to use merge() instead of replace() in your code, i.e.:
$this->merge( ['title' => trim(strip_tags($input['title']))] );
Second option is to override the all() function, i.e.:
public function all()
{
$input = parent::all();
if( !empty($input) )
{
$input['title'] = trim(strip_tags($input['title']));
}
return $input;
}
In the last code example you dont have to use the sanitize() function.