Backpack V4 modifying field before store - laravel

In 3.6 version of backpack I can change an attribute value before storing it.
I have this code
If ($request->description == "") {
$request->description="User has not entered any description";
}
$redirect_location = parent::storeCrud($request);
What can I do to get the same in V4? I'm reading this guide but I can't make it to work.
This is what I'm trying in V4
public function store(PedidoRequest $request)
{
Log::debug('testing...');
If ($request->description == "") {
$request->description="User has not entered any description";
}
$redirect_location = $this->traitStore();
return $redirect_location;
}

The Request object in Laravel, Illuminate\Http\Request, doesn't have the ability to set the inputs via the properties like that, no __set method ($request->description = '...' does not set an input named description). You would have to merge the inputs into the request or use the array syntax to do that:
$request->merge(['description' => '...']);
// or
$request['description'] = '...';
But since backpack seems to have abstracted things apparently you aren't controlling anything in your controller methods you could try this:
$this->crud->request->request->add(['description'=> '...']);
Potentially:
$this->request->merge(['description' => '...']);
That would be assuming some trait the Controller uses is using the Fields trait.

Related

Creating a Laravel attribute (accessor) on model but unable to access model properties

Here's my code:
protected function expires(): Attribute
{
if ($this->started_at) {
$expiry = $this->started_at->addDays(20);
}
return Attribute::make(
get: fn () => $expiry ?? null
);
}
Running this code gives me an ErrorException with the message Undefined property: Models\Job::$started_at
I have found that I can work around this error by accessing the property through $this->attributes['started_at'] as follows:
protected function expires(): Attribute
{
if ($this->attributes['started_at']) {
$expiry = Carbon::parse($this->attributes['started_at'])->addDays(20);
}
return Attribute::make(
get: fn () => $expiry ?? null
);
}
However, this code feels a little inefficient because I'm manually using Carbon to parse the property back into a Carbon object. But if I do a dd($this->started_at) right before the if statement, it's already been cast to a Carbon object by Laravel and I'd really just like to use this object to make my code as clean as in the first example above.
I'd like to know the reason why $this->started_at is apparently available as a Carbon object in this context but somehow not usable (an undefined property) in the way I'm using it, and also I would like to know if there is another way to go about achieving my goal?
you can add custom attributes with
public function getExpireAttribute()
{
if ($this->started_at) {
$this->started_at->addDays(20);
}
return $this->started_at;
}
now you can access expire attribute like other, with
$model->expire
to make Eloquent casts dates to Carbon for you, add attribute to casts:
protected $casts = [
'started_at' => 'datetime',
];
The reason you are getting an "Undefined property" error when trying to access $this->started_at in your accessor method is because Laravel's model accessor methods are executed before the model attributes are hydrated.
This means that when your expires() method is executed, the started_at attribute may not have been set yet, and thus accessing it directly on the model instance will result in an "Undefined property" error.
One way to work around this is to use the getAttribute method provided by Laravel's Model class. This method allows you to retrieve the value of an attribute, even if it has not been set yet. Here's an updated version of your expires() method that uses getAttribute:
use Carbon\Carbon;
protected function getExpiresAttribute(): ?Carbon
{
$startedAt = $this->getAttribute('started_at');
if ($startedAt) {
return $startedAt->addDays(20);
}
return null;
}
In this version, we are using the getAttribute method to retrieve the value of the started_at attribute, even if it has not been set yet. We then use Carbon to manipulate the date, and return the result.
Note that we are using the getExpiresAttribute method instead of the expires method, because Laravel automatically maps get{AttributeName}Attribute method calls to corresponding attribute accessors. So, in this case, calling
$model->expires
will automatically execute the getExpiresAttribute method.
With this approach, you can use the started_at property directly in your code, and it will be automatically cast to a Carbon object by Laravel, without the need to manually parse it with Carbon.
Hope this helps.

How can i get the buyers to still be able to purchase the product even if its unavailable?

I'm trying to create waitlist to buy product, but change the status of the transaction from 'Confirmed' to 'Waitlist'.
For that I have added a variable 'status' and a function in Transaction Model as follows:
const CONFIRMED_TRANSACTION = 'confirmed';
const WAITLIST_TRANSACTION = 'waitlist'; //waitlist product
public function isConfirmed() {
return $this->status == Transaction::CONFIRMED_TRANSACTION;
}
Default value of Transaction->status is 'CONFIRMED_TRANSACTION'.
Now every time the Product->status changes to 'unavailable' i would like to change the value of the Transaction->status to 'WAITLIST_TRANSACTION' when the transaction is created.
I am trying to achieve it using Event Listeners as:
Transaction::created(function($transaction) {
if(!$product->isAvailable()) {
$transaction->status = Transaction::WAITLIST_TRANSACTION;
$transaction->save();
}
});
But this gives me error :
**ErrorException: Undefined variable: product in file /home/vagrant/restfulapi7/app/Providers/AppServiceProvider.php on line 29**
How can I achieve the same in a better way?
You're using what's called a lambda or anonymous function, which has a different scope to the rest of your code.
function($transaction) does not have access to a variable named $product. So you're trying to call ->isAvailable() on a variable which doesn't exist.
Transaction::created(function($transaction) {
if(!$product->isAvailable()) {
$transaction->status = Transaction::WAITLIST_TRANSACTION;
$transaction->save();
}
});
Assuming it exists in the scope in which you call Transaction::created(), you can pass the $product variable into your lambda function like this:
function($transaction) use ($product) { ... }
For more information on lambda functions, read the php docs here
This solves the issue:
if(!$transaction->product->isAvailable())

The update nor the destroy methods won't work in laravel eloquent model?

I have a strange situation where eloquent model won't let me update nor destroy while index and create is working fine!
I'm using Vue.js and Laravel API Resource for form control, and while it worked with me before, it won't work here:
Here's my Vue.js Code:
updateFinish(finish) {
axios.patch(`/api/finishes/${finish.id}`, finish).then(response => {
this.fetchFinishes();
}).catch(error => {
// Get laravel validation error
this.errors = error.response.data.errors;
});
},
laravel update code (not working)
public function update(FinishType $finishType)
{
// Don't know why not working
$finishType->update($this->validateRequest());
return new FinishTypeResource($finishType);
}
the response is null:
{"id":null,"name":null}
While this code works:
public function update($id)
{
$finishType = FinishType::find($id);
$validates = $this->validateRequest();
$finishType->name = $validates['name'];
$finishType->save();
return new FinishTypeResource($finishType);
}
public function validateRequest()
{
return request()->validate([
'name' => 'required | unique:finish_types',
]);
}
Note the Model name is FinishType and database table name is finish_types, I even tried to define the table name in the model like so protected $table = 'finish_types' – still not working and I already have defined the $fillable array!!!
Your route model binding is not working correctly, for the implicit binding to work your injected variable should match the route parameter name.
Assuming that your parameter name could be finish (reading the url from your javascript) you have to write the update function using $finish as injected variable, like this:
public function update(FinishType $finish)
{
$finish->update($this->validateRequest());
return new FinishTypeResource($finish);
}
Do the same for destroy():
public function destroy(FinishType $finish)
{
// your destroy code here
}
In any case you can run php artisan route:list to find your parameter name (the part of the URI in braces) and give the same name to the injected variable.
If the two do not match, parameter and injected variable name, laravel injects a void, not loaded, FinishType model so it does not make sense doing an update or a delete on it.
I can't post comments so I'm going to post what I assume is the answer.
Laravel does route model binding automagically when the route url name corresponds to the name of the table I think... or model.
So users/{id} would auto bind the User object when you type it as a param in the controller. Example (User $user)
However, since your URL seems to be "different" from the name of your Model/Table, go to the RouteServiceProvider, and manually do the binding.
So in your case you'd do something like this in the boot function of the RouteServiceProvider class:
public function boot()
{
parent::boot();
Route::model('finishes', FinishType::class);
}
Don't forget your imports :)
You can read more about Explicit Model Binding here: https://laravel.com/docs/5.8/routing#explicit-binding

Laravel Model Get Method Decoding JSON

Hi there below is what I'm storing in my db but when I use my get method in my model I have to use json_decode twice when formating my data why is this happening and can I have it just use it once somehow.
json exactly in db:
"[{\"id\":\"1\",\"country\":\"New Zealand\",\"shipping_rate\":\"1\"},{\"id\":\"2\",\"country\":\"Australia\",\"shipping_rate\":\"2\"}]"
Model Get Method:
public function getshippingAttribute()
{
return $this->attributes['shipping'] ? json_decode(json_decode($this->attributes['shipping'])) : [];
}
The problem is not clear enough from your question but the Laravel offers a builtin mechanism for attribute casting (Since v-5.1). In this case, in your model, just declare a $casts property for example:
protected $casts = [
'shipping' => 'array',
// more ...
];
Because of the $casts property given above, whenever you'll write (create/update) a model, you don't need to explicitly use json_encode to convert the array to json string, Laravel will do it for you and also, when you'll retrieve the model (single/collection), the shipping attribute will be automatically converted back to an array so you don't need to use json_decode for working with the attribute.
Regarding the response, that will be also handled by laravel if you don't convert it to json manually (when returning a model/collection). This will possibly solve your problem.
public function getshippingAttribute()
{
return $this->attributes['shipping'] ? json_decode($this->attributes['shipping']) : [];
}
Try return json response
public function getshippingAttribute()
{
return response()->json($this->attributes['shipping'])
}

Yii2 : How to validate XSS (Cross Site Scripting) in form / model input?

Yii2 has support for XSS(cross-site-scripting ) validation of displayed data using the helper class\yii\helpers\HtmlPurifier, however this only validates and cleans up output code like this
echo HtmlPurifier::process($html);
How to validate input for XSS of input such that this data is not stored in the database itself ?
This can be done using a filterValidator by calling the process as named callable function of validation like this
class MytableModel extends ActiveRecord {
....
public function rules(){
$rules = [
[['field1','field2'],'filter','filter'=>'\yii\helpers\HtmlPurifier::process']
];
return array_merge(parent::rules(),$rules);
}
....
}
Where field1, field2 etc are the inputs fields to be validated, the same applies for Form Model validations as well
in the before validate method add the following:
public function beforeValidate()
{
foreach (array_keys($this->getAttributes()) as $attr){
if(!empty($this->$attr)){
$this->$attr = \yii\helpers\HtmlPurifier::process($this->$attr);
}
}
return parent::beforeValidate();// to keep parent validator available
}
it will help you if you want to run Xss Validator before validate/save all attributes nested of adding the following line
return array_merge(parent::rules(),$rules);
to every class extends the new active record

Resources