How do I update an eloquent json column from validation data without over writing what's stored in the column? - json

I'm hoping for a straight-forward solution to do this, but so far I've been coming up empty...
I have a front end vue app/form that sends data back to my laravel backend - I have a controller that validates and saves the request (not looking for feedback on this architecture at the moment unless it actually solves the problem - that's a task for another day...)
I've added a json column called "custom_redeem_fields"
For context, it's to support more flexibility and accepts key/val pairs to use in another field called "custom_redeem_instructions" that has text with delimiters for each of the keys from "custom_redeem_fields", although, I'd prefer to keep from defining these keys statically because the whole point is to be able to add new keys at will. So custom_redeem_instructions will read something like "please visit {•URL•} and enter code {•CODE•}..." and those values will come from the custom_redeem_fields json field.
In the model, I have "custom_redeem_fields" in the fillable array, as well as set as castable to json.
protected $fillable = ['custom_redeem_fields'];
protected $casts = ['custom_redeem_fields' => 'json'];
In the controller, I have ~20 additional columns (not really relevant here, so I've only included two) so I'm trying not to call them out individually beyond their validation rules. The request typically sends one field at a time, so the user can update and save each field as they go. This was working appropriately for all the other fields I had before I added the "custom_redeem_fields.xxxx" to the mix.
$validatedData = $request->validate([
'title' => 'sometimes|required|max:255',
'text' => 'sometimes|required_unless:redeem_type,9|max:255',
'custom_redeem_fields.email' => 'sometimes|email',
'custom_redeem_fields.phone' => ['sometimes', new ValidPhone],
'custom_redeem_fields.code' => 'sometimes',
'custom_redeem_fields.url' => 'sometimes|url'
]);
$ticket = Ticket::find($id)
$ticket->update($validatedData);
Now, with the "custom_redeem_fields.xxxxx" this falls apart - the entire json object stored in "custom_redeem_fields" is overwritten with the most recent update, rather than just updating the key included in the validatedData array. So if I save:
[
"title" => "Monty Pythons Flying Circus"
"text" => "Monty Pythons Flying Circus is a British surreal sketch comedy series created by and starring the comedy group Monty Python, consisting of Graham Chapman, ..."
"custom_redeem_fields" => [
"email" => "bob#example.com",
"phone" => "503.555.5555",
"code" => "1xoicvjq",
"url" => "https://example.com/"
]
]
and then I send:
"custom_redeem_fields" => ["email" => "pat#example.com"]
the custom redeem fields returns:
"custom_redeem_fields" => ["email" => "pat#example.com"]
rather than:
"custom_redeem_fields" => ["email" => "pat#example.com", "phone" => "503.555.5555", "code" => "1xoicvjq", "url" => "https://example.com/"]
It seems that validation rules need json keys to be notated with dot syntax (custom_redeem_fields.url), and eloquent needs arrow syntax (custom_redeem_fields->url), but I'm not sure what's the most straightforward way to transition between the two, which seems very not-laravel, and the documentation is certainly lacking in this department...
Any help would be appreciated.
Thanks!

Wouldn't array_merge() solve your problem, it would overwrite values you provide with the second parameter. If you give it the already existing ones as the first, it would combine the two as you want.
$customRedeemInput = [...];
$model->custom_redeem_fields = array_merge($model->custom_redeem_fields, $customRedeemInput);
$model->save();

Related

How to implement Filtering on YII restful GET api?

I am working on Restful APIs of Yii.
My controller name is ProductsController and the Model is Product.
When I call API like this GET /products, I got the listing of all the products.
But, now I want to filter the records inside the listing API.
For Example, I only want those records Which are having a product name as chairs.
How to implement this?
How to apply proper filtering on my Rest API. I am new to this. So, I have no idea how to implement this. I also followed their documentation but unable to understand.
May someone please suggest me a good example or a way to achieve this?
First of all you need to have validation rules in your model as usual.
Then it's the controllers job and depending on the chosen implementation I can give you some hints:
If your ProductsController extends yii\rest\ActiveController
Basically the easiest way because almost everything is already prepared for you. You just need to provide the $modelClass there and tweak actions() method a bit.
public function actions()
{
$actions = parent::actions();
$actions['index']['dataFilter'] = [
'class' => \yii\data\ActiveDataFilter::class,
'searchModel' => $this->modelClass,
];
return $actions;
}
Here we are modifying the configuration for IndexAction which is by default responsible for GET /products request handling. The configuration is defined here and we want to just add dataFilter key configured to use ActiveDataFilter which processes filter query on the searched model which is our Product. The other actions are remaining the same.
Now you can use DataProvider filters like this (assuming that property storing the product's name is name):
GET /products?filter[name]=chairs will return list of all Products where name is chairs,
GET /products?filter[name][like]=chairs will return list of all Products where name contains word chairs.
If your ProductsController doesn't extend yii\rest\ActiveController but you are still using DataProvider to get collection
Hopefully your ProductsController extends yii\rest\Controller because it will already benefit from serializer and other utilities but it's not required.
The solution is the same as above but now you have to add it by yourself so make sure your controller's action contains something like this:
$requestParams = \Yii::$app->getRequest()->getBodyParams(); // [1]
if (empty($requestParams)) {
$requestParams = \Yii::$app->getRequest()->getQueryParams(); // [2]
}
$dataFilter = new \yii\data\ActiveDataFilter([
'searchModel' => Product::class // [3]
]);
if ($dataFilter->load($requestParams)) {
$filter = $dataFilter->build(); // [4]
if ($filter === false) { // [5]
return $dataFilter;
}
}
$query = Product::find();
if (!empty($filter)) {
$query->andWhere($filter); // [6]
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]); // [7]
What is going on here (numbers matching the code comments):
We are gathering request parameters from the body,
If these are empty we take them from the URL,
We are preparing ActiveDataFilter as mentioned above with searched model being the Product,
ActiveDataFilter object is built using the gathered parameters,
If the build process returns false it means there is an error (usually unsuccessful validation) so we return the object to user to see list of errors,
If the filter is not empty we are applying it to the database query for Product,
Finally we are configuring ActiveDataProvider object to return the filtered (and paginated and sorted if applicable) collection.
Now you can use DataProvider filters just as mentioned above.
If your ProductsController doesn't use DataProvider to get collection
You need to create your custom solution.

Magento 1.9 add custom column to associated products grid

We have an existing customization that appears to have broken when we upgraded from 1.7 to 1.9 community.
The customization adds a column to the associated products grid.
The customization is a local override of
app/code/core/Mage/Adminhtml/Block/Catalog/Product/Edit/Tab/Super/Group.php
This was done before I started on the project
$this->addColumn('breakdown_part_no', array(
'header' => Mage::helper('catalog')->__('Part No'),
'name' => 'breakdown_part_no',
'type' => 'varchar',
'index' => 'breakdown_part_no',
'width' => '120px',
'editable' => true,
));
This was added to _prepareColumns()
Another customization was added to method getSelectedGroupedProducts()
public function getSelectedGroupedProducts()
{
$associatedProducts = Mage::registry('current_product')->getTypeInstance(true)
->getAssociatedProducts(Mage::registry('current_product'));
$products = array();
foreach ($associatedProducts as $product) {
$products[$product->getId()] = array(
'qty' => $product->getQty(),
'position' => $product->getPosition(),
'breakdown_part_no' => $product->getBreakdownPartNo(),
);
}
return $products;
}
The behavior is that the column appears in the admin and can be edited, however when saved, it does not save any value.
If I modify the getSelectedGroupedProducts part and set a hard coded value, it displays still no value (blank field), but interestingly if I click save with no value, it saves the value that was hard coded. If I enter any value in the field, it saves as a blank. This is really strange behavior that makes no sense to me.
If I change one of the other fields, such as position to be a hard coded value, it appears instantly and works as expected. Please let me know the proper way for this to work.
There are several posts on various forums about how to do the above and the modification mentioned is true, but what all of the other posts left out was the adminhtml layout input. When a user edits product data in Magento Admin (Associated Products), the data is serialized and sent to the controller save action. I noticed that the fields were not present when a value was entered. This is because the value wasn't in the layout so it was being stripped off of the request before it was posted to the controller.
Add input field in adminhtml/default/default/layout/catalog.xml
adminhtml_catalog_product_supergroup
addColumnInputName

The default remember_me token in Laravel is too long

TL;DR How can I use my own way of generating the remember_me token?
I have an old site, written without any framework, and I have been given the job to rewrite it in Laravel (5.4.23). The DB is untouchable, cannot be refactored, cannot be modified in any way.
I was able to customise the Laravel authentication process using a different User model, one that reflect the old DB. But when it comes to the "Remember me" functionality, I have an issue with the length of the token.
The old site already uses the "Remember me" functionality but its DB field has been defined as BINARY(25). The token generated by the SessionGuard class is 60 characters long.
My first attempt was to try and find a way to shorten the token before writing it into the DB, and expand it again after reading it from the DB. I couldn't find such a way (and I'm not even sure there is such a way).
Then I looked into writing my own guard to override the cycleRememberToken (where the token is generated). I couldn't make it work, I think because the SessionGuard class is actually instantiated in a couple of places (as opposed to instantiate a class based on configuration).
So, I am stuck. I need a shorten token and I don't know how to get it.
Well, I was on the right track at one point.
I had to create my own guard, register it and use it. My problem, when I tried the first time, was that I did not register it in the right way. Anyway, this is what I did.
I put the following in AuthServiceProvides
Auth::extend('mysession', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new MyGuard('lrb', $provider, app()->make('session.store'));
$guard->setCookieJar($this->app['cookie']);
$guard->setDispatcher($this->app['events']);
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
return $guard;
});
I change the guard in config/auth.php as
'guards' => [
'web' => [
'driver' => 'mysession',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
and finally my new guard
class MyGuard extends SessionGuard implements StatefulGuard, SupportsBasicAuth
{
/**
* #inheritdoc
*/
protected function cycleRememberToken(AuthenticatableContract $user)
{
$user->setRememberToken($token = Str::random(25));
$this->provider->updateRememberToken($user, $token);
}
}

yii2 and select2 for tags

I'm using kartik select2 widget like this:
echo Select2::widget([
'model' => $model,
'attribute' => 'script_tags',
'data' => $model->tagList,
'options' => ['multiple' => true,'placeholder' => 'Select states ...'],
'pluginOptions' => [
'tags' => true
],
]);
$model->tagList is array in this format ['id'=>'name'] populated from database.
My question is what is the best why to save it into db table because with custom tags i have response for script_tags like this
[0=>'1', 1=>'5', 2=>'math'],
i need to save new tag in table tag[fields=id, name] and relations for all tags in table tagmap[fields=id, script_id, tag_id]
should i check if is integer save relations in tagmap, if is string save first new tag in tag table then save relation in tagmap
Your approach looks fine, except when somebody explicitly enters a number as tag value. Let's imagine in your tag table ID 1 is 'foo' and ID 5 is 'bar'. There's no way to tell if you got a 'foo,bar,math' or 'foo,5,math'. You could check if those IDs actually exist in your database and act accordingly, but that's probably overkill.
However, I suggest you take a look at some tagging solutions that already exist. I'm pretty happy with 2amigos/yii2-taggable-behavior, but there's also creocoder/yii2-taggable and probably many others.
As an added benefit, 2amigos taggable also stores tag frequencies.
Keep in mind that having an id column in tagmap, which I bet is an autoincrement PK, is bad practice, you should use (script_id, tag_id) as a composite PK.

how to store instance methods name of a class in a column in sql database?

I want to create a column that has nested hash structure.
structure of that column:
column_name: company
{ production=> {watch=> "yes",pen =>"no",pencil => "yes"}
quality => {life_test =>"yes", strong_test =>"yes", flexibility_test => "no"}
}
Here production, quality are my models and watch, pen, pencil, life_test,strong_test are my instance method of respective classes. each instance method will get the Boolean value from the view page.
How to achieve this structure.
This is called serialization and it is pretty easy. You could do the following:
class Something < ActiveRecord::Base
serialize :company, JSON
end
bar = Something.new
bar.company = { :production=> {:watch=> true,:pen => false, :pencil => false}
:quality => {:life_test =>true, :strong_test =>true, :flexibility_test => false} }
bar.save
If you want more info go here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html and read the part on "Saving arrays, hashes, and other non-mappable objects in text columns" just make sure your company column in the database is a text column.
Use serialization to store hash in db .
Follow api link