How to migrate from string column to foreign key pointing to row in other table with same string value in Laravel? - sql

I am using Laravel 5.6 and need some help migrating a column from a populated table preserving the content logic. There is a table pages with a column named icon that accepts string values.
Ex:
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
...
$table->string('icon')->nullable();
}
The pages table is populated and the icon column, being nullable, is not always used.
A new icons table was created to store all the usable icon classes.
Ex:
Schema::create('icons', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
});
How can i migrate the icon column from the pages table to be a foreign key that points to the icons table row that has the same value in the name column or null if not populated?

I'd suggest a polymorphic many-to-many approach here so that icons are reusable and don't require a bunch of pivot tables, should you want icons on something other than a page.
Schema::create('icons', function(Blueprint $table) {
$table->increments('id');
$table->string('name');
});
Schema::create('iconables', function(Blueprint $table) {
$table->integer('icon_id');
$table->integer('iconables_id');
$table->integer('iconables_type');
});
Now you just need to determine if the pages have an existing Icon. If they do, then hold reference to them so you can insert them:
$pagesWithIcons = Page::whereNotNull('icon')->get();
At this point you need to define the polymorphic relations in your models:
// icon
class Icon extends Model
{
public function pages()
{
return $this->morphedByMany(Page::class, 'iconable');
}
}
// page
class Page extends Model
{
public function pages()
{
return $this->morphToMany(Icon::class, 'iconable');
}
}
Now you just need to create the icons (back in our migration), and then attach them if they exist:
$pagesWithIcons->each(function(Page $page) {
$icon = Icon::firstOrNew([
'name' => $page->icon
});
$icon->pages()->attach($page);
});
The above is creating an Icon if it doesn't exist, or querying for it if it does. Then it's attaching the page to that icon. As polymorphic many-to-many relationships just use belongsToMany() methods under the hood, you have all of the available operations at your leisure if this doesn't suite your needs.
Finally, drop your icons column from pages, you don't need it.
Schema::table('pages', function(Blueprint $table) {
$table->dropColumn('icon');
});
And if you need to backfill support for only an individual icon (as the many-to-many will now return an array relationship), you may add the following to your page model:
public function icon()
{
return $this->icons()->first();
}
Apologies if typos, I did this on my phone so there may be some mistakes.

Related

Extending Shopware entity with foreign keys fails when merging version

I'm developing my first Shopware 6 admin plugin, for which is required to extend one of the existing Shopware plugins - Custom products.
I want to add a relation between already existing entities - TemplateExclusion and TemplateOptionDefinition. When I select from the UI my options, TemplateExclusion entity its getting saved in the database, without any problems.
When I save the Template entity (parent of TemplateExclusion), my "excluded_option_id" its getting overwritten with the 1st possible option from TemplateOptionDefinition entities.
I have notice that this is happening on "mergeVersion". Also when I try to save the Template entity with debug mode enabled and profiler, I'm getting an error during saving, that "excludedOptionId" is blank when merging, which is not true.
Error in profiler
Following the documentation I made first the migration:
class Migration1643023742TemplateExclusionRelation extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1643023742;
}
public function update(Connection $connection): void
{
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD COLUMN `excluded_option_id` BINARY(16) AFTER `template_version_id`;');
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD COLUMN `excluded_option_version_id` BINARY(16) AFTER `excluded_option_id`;');
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD CONSTRAINT `fk.swag_cupr_template_exclusion.excluded_option_id` FOREIGN KEY (`excluded_option_id`, `excluded_option_version_id`)
REFERENCES `swag_customized_products_template_option` (`id`, `version_id`) ON DELETE CASCADE ON UPDATE CASCADE;');
}
then I made an entity extension, where to define the new fields.
class TemplateExclusionExtension extends EntityExtension
{
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('excluded_option_id', 'excludedOptionId', TemplateOptionDefinition::class))
->addFlags(new Required(), new ApiAware())
);
$collection->add(
(new ManyToOneAssociationField('excludedOption', 'excluded_option_id', TemplateOptionDefinition::class))
->addFlags(new ApiAware())
);
$collection->add(
(new ReferenceVersionField(TemplateOptionDefinition::class, 'excluded_option_version_id'))
->addFlags(new Required(), new ApiAware()),
);
}
public function getDefinitionClass(): string
{
return TemplateExclusionDefinition::class;
}
}
Solved:
It was wrong definition from my side:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('excluded_option_id', 'excludedOptionId', TemplateOptionDefinition::class))
->addFlags(new Required(), new ApiAware())
);
$collection->add(
(new OneToOneAssociationField(
EasyExtendCustomizedProducts::TEMPLATE_EXCLUSION_EXCLUDED_OPTION_EXTENSION,
'excluded_option_id',
'id',
TemplateOptionDefinition::class,
false
))->addFlags(new CascadeDelete(), new ApiAware())
);
}
public function getDefinitionClass(): string
{
return TemplateExclusionDefinition::class;
}
If I'm not mistaken the issue was the missing CascadeDelete delete flag.
To versionize the entity it is first fetched including its associated data and is then persisted with new primary keys, so basically it gets cloned. However not all associations are taken into account when fetching the data to be cloned. You can find the responsible code here, where the affected associations get filtered by the existence of the CascadeDelete flag. If they miss the flag they will be ignored for creating the cloned version. This behavior still needs to be documented more prominently.

Laravel Relationships 2 tables

I have two tables which I want to connect,
First table = Friendship
ID
User1 = Tim
User2 = Johny
Accepted = 0/1 <- friends if accepted = 1
Second table = Rooms
ID
Owner
Room_ID
Roome_name
My goal is to get all Johny friends then check if any of them has Rooms if yes retrieve owner, room_id, room_name. I searched result in google but I could not find it. It's my first time with relationships and I don't know how to use where statments there. I would be greateful for simple and clear advice.
Here are my classes:
class Friendship extends Eloquent {
protected $table = 'friendship';
public function friendrooms()
{
return $this->hasMany('Room');
}
}
class Room extends \Eloquent {
protected $table = 'rooms';
public function roomowner()
{
return $this->belongsTo('Friendship');
}
}
Sorry for my bad english.
Assuming you have a users table, you'd want to use your friends table as a pivot table for users onto itself. It sounds quite complicated, but it ends up being pretty easy in practice...
I modified a few of your columns because there were a few things that didn't make a lot of sense. Not sure why rooms needed an id column and a room_id column. This should get you a pretty good base and it's hopefully fairly extensible for you. You'd probably want a room_user table which stores who is in what room.
Migrations
Schema::create('friends', function($table)
{
$table->increments('id');
$table->integer('user_id');
$table->integer('friend_id');
$table->boolean('accepted');
$table->boolean('deleted');
$table->timestamps();
});
Schema::create('rooms', function($table)
{
$table->increments('id');
$table->integer('user_id'); // The is room's owner.
$table->string('description');
$table->integer('room_id');
$table->string('room_name');
$table->timestamps();
});
User Model
class User extends Eloquent implements UserInterface, RemindableInterface {
public function friends()
{
return $this->belongsToMany('User', 'friends', 'user_id', 'friend_id')->wherePivot('accepted', '1');
}
public function room()
{
return $this->hasOne('Room');
}
public function hasRoom()
{
return (bool)$this->room()->count();
}
}
Use
$user = User::find(1);
foreach($user->friends as $friend) {
if($friend->hasRoom()) {
echo "<a href='javascript:location.href='ts3server://localhost/?port=9987&cid=".$friend->room->room_id."'>Join ".$friend->room->name."</a>";
}
}
If you need anymore help, ask away.
Edits:
If someone can have many rooms, simply change that relationship to a hasMany(). Then you would have to use it just a bit differently...
$user = User::find(1);
$friends = $user->friends()->paginate(15);
foreach($friends as $friend) {
if($friend->hasRoom()) {
foreach($friend->rooms as $room) {
echo "<a href='javascript:location.href='ts3server://localhost/?port=9987&cid=".$friend->room->room_id."'>Join ".$friend->room->name."</a>";
}
}
}
The logic for the 3 rooms per day doesn't really belong here. That would be more of a validation issue when allowing them to create rooms.
That's all the beauty in the Eloquent. You don't have to put a where in no place!! You just call something like
$friends = Friendship::find($friend_id);
$friends->friendrooms()->get(); //To get a list of all rooms
... yes, it's that simple!
The second you put a belongsTo or hasMany, the Eloquent already tells Laravel it should perform one kind of operation on the query: where id = 'child_id' and where = foreign_id = "id", respectively. I don't know if I have made myself clear. Any doubts, just comment! =D

How to save content from yii widget to Database?

here is the ListBuilder Widget code:
$this->widget('ext.widgets.multiselects.XMultiSelects',array(
'leftTitle'=>'Australia',
'leftName'=>'Person[australia][]',
'leftList'=>Person::model()->findUsersByCountry(14),
'rightTitle'=>'New Zealand',
'rightName'=>'Person[newzealand][]',
'rightList'=>Person::model()->findUsersByCountry(158),
'size'=>20,
'width'=>'200px',
));
List Builder view this
I wanna save that entire list i select to right list on to my DB.
how to do this?
The source code is available so you just have to look in the controller:
public function actionMovePersons()
{
if(isset($_POST['Person']['australia']))
{
foreach ($_POST['Person']['australia'] as $id)
Person::model()->updateUserCountry($id, 14);
}
if(isset($_POST['Person']['newzealand']))
{
foreach ($_POST['Person']['newzealand'] as $id)
Person::model()->updateUserCountry($id, 158);
}
Yii::app()->user->setFlash('saved',Yii::t('ui','Data successfully saved!'));
$this->redirect(array('site/extension','view'=>'listbuilder'));
}
And in the model:
public function updateUserCountry($id, $country_id)
{
$model=$this->findByPk($id);
$model->country_id=$country_id;
$model->update('country_id');
}
The above updates the Person in the database and sets the new country. If I understand your question correct you want to create new entries (maybe in a different table) instead of changing existing ones. To do this you can easily modify the updateUserCountry() function to create a new Person instead of updating it.

Yii form and model for key-value table

I have a table which has only two column key-value. I want to create a form which allow user insert 3 pair of key-value settings.
Do I need pass 3 different models to the view? Or is there any possible way to do this?
Check out this link:
http://www.yiiframework.com/doc/guide/1.1/en/form.table
This is considered best form in Yii for updating for creating multiple models.
In essence, for creation you can create a for loop generate as many inputs a you wish to have visible, and in your controller loop over the inputs to create new models.
View File:
for ( $settings as $i=>$setting ) //Settings would be an array of Models (new or otherwise)
{
echo CHtml::activeLabelEx($setting, "[$i]key");
echo CHtml::activeLabelEx($setting, "[$i]key");
echo CHtml::error($setting, "[$i]key");
echo CHtml::activeTextField($setting, "[$i]value");
echo CHtml::activeTextField($setting, "[$i]value");
echo CHtml::error($setting, "[$i]value");
}
Controller actionCreate:
$settings = array(new Setting, new Setting, new Setting);
if ( isset( $_POST['Settings'] ) )
foreach ( $settings as $i=>$setting )
if ( isset( $_POST['Setttings'][$i] ) )
{
$setting->attributes = $_POST['Settings'][$i];
$setting->save();
}
//Render View
To update existing models you can use the same method but instead of creating new models you can load models based on the keys in the $_POST['Settings'] array.
To answer your question about passing 3 models to the view, it can be done without passing them, but to validate data and have the correct error messages sent to the view you should pass the three models placed in the array to the view in the array.
Note: The example above should work as is, but does not provide any verification that the models are valid or that they saved correctly
I'm going to give you a heads up and let you know you could potentially make your life very complicated with this.
I'm currently using an EAV patterned table similar to this key-value and here's a list of things you may find difficult or impossible:
use CDbCriteria mergeWith() to filter related elements on "value"s in the event of a search() (or other)
Filtering CGridView or CListView
If this is just very straight forward key-value with no related entity aspect ( which I'm guessing it is since it looks like settings) then one way of doing it would be:
create a normal "Setting" CActiveRecord for your settings table (you will use this to save entries to your settings table)
create a Form model by extending CFormModel and use this as the $model in your form.
Add a save() method to your Form model that would individually insert key-value pairs using the "Setting" model. Preferably using a transaction incase a key-value pair doesn't pass Settings->validate() (if applicable)
optionally you may want to override the Form model's getAttributes() to return db data in the event of a user wanting to edit an entry.
I hope that was clear enough.
Let me give you some basic code setup. Please note that I have not tested this. It should give you a rough idea though.:
Setting Model:
class Setting extends CActiveRecord
{
public function tableName()
{
return 'settings';
}
}
SettingsForm Model:
class SettingsForm extends CFormModel
{
/**
* Load attributes from DB
*/
public function loadAttributes()
{
$settings = Setting::model()->findAll();
$this->setAttributes(CHtml::listData($settings,'key','value'));
}
/*
* Save to database
*/
public function save()
{
foreach($this->attributes as $key => $value)
{
$setting = Setting::model()->find(array('condition'=>'key = :key',
'params'=>array(':key'=>$key)));
if($setting==null)
{
$setting = new Setting;
$setting->key = $key;
}
$setting->value = $value;
if(!$setting->save(false))
return false;
}
return true;
}
}
Controller:
public function actionSettingsForm()
{
$model = new Setting;
$model->loadAttributes();
if(isset($_POST['SettingsForm']))
{
$model->attributes = $_POST['SettingsForm'];
if($model->validate() && $model->save())
{
//success code here, with redirect etc..
}
}
$this->render('form',array('model'=>$model));
}
form view :
$form=$this->beginWidget('CActiveForm', array(
'id'=>'SettingsForm'));
//all your form element here + submit
//(you could loop on model attributes but lets set it up static for now)
//ex:
echo $form->textField($model,'fieldName'); //fieldName = db key
$this->endWidget($form);
If you want further clarification on a point (code etc.) let me know.
PS: for posterity, if other people are wondering about this and EAV they can check the EAV behavior extention or choose a more appropriate DB system such as MongoDb (there are a few extentions out there) or HyperDex

Object wrappers and forms

I am developing an ecommerce platform and came across a difficulty. Basically, I have in my scenario a Product, Option and OptionValue. A product might have multiple options which might have multiple values. The problem is how to store it in a way that is easy to create and edit.
The problem is I've a direct reference to the OptionValue, which is mutable. I need to keep immutable information about the Option and OptionValue (for example, if a order was made and the color was green, even if this option is changed to lime green, the order must keep showing as green). In that case, I need to save some properties of Option (the option name - "Colors" for example) and of each OptionValue (the value of each option - "red" for example). The way I thought, it would require a structure very similar to the existing structure: a new class ItemOption referencing Option and a ItemOptionValue referencing OptionValue.
So, this was my attempt:
To create a form to display my options I have:
class OptionSelectorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
foreach ($options['product']->getOptions() as $option) {
$builder->add($option->getId(), 'choice', array('choice_list' => new ObjectChoiceList($option->getValues());));
}
}
}
I'm using a DataTransformer to convert a collection of OptionValue in a collection of OrderItemOption:
class OrderItemOptionToOptionValueTransformer implements DataTransformerInterface
{
public function transform($lineOptions)
{
if(!$lineOptions) {
return array();
}
$values = array();
foreach($lineOptions as $lineOption) {
$lineOption->getOption()->getId();
$values[$id] = array();
foreach($lineOption->getValues() as $lineOptionValue) {
$values[$id][] = $lineOptionValue->getOptionValue();
}
}
return $values;
}
public function reverseTransform($values)
{
$collection = new ArrayCollection();
foreach($values as $optionId => $optionValues) {
if(!$optionValues) {
continue;
}
$lineOption = new OrderItemOption();
$optionValues = is_array($optionValues) ? $optionValues : array($optionValues);
foreach($optionValues as $optionValue) {
$lineOptionValue = new OrderItemOptionValue();
$lineOptionValue->setOptionValue($optionValue);
$lineOption->addValue($lineOptionValue);
}
$lineOption->setOption($optionValue->getOption());
$collection->add($lineOption);
}
return $collection;
}
}
Finally, my OrderItemType form:
class OrderItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('options', 'option_selector', array('options' => $options['options']))
->addModelTransformer(new OrderItemOptionToOptionValueTransformer())
);
}
}
It works but doesn't seem to me a good approach, once the OptionValue will be always recreated, never updated.
How would you do it?
I will keep it simple. You can stick with your structure but instead of transforming things on the fly, keep them at the same state.
What do I mean?
Whenever an Option is created, create the corresponding ItemOption. The same goes with the OptionValue and ItemOptionValue. The relation between the two is a one-to-one connection, whereas the Option and OptionValue don't know the connected ItemOption and ItemOptionValue.
Now if a change occurs to the OptionValue, you can query for the connected ItemOptionValue and change the things you need to change (depends on internal structure).
How to store the connection?
Use whatever persistence method you already use. For the case of a database, store the connection in one table like this:
CREATE TABLE item_option_to_option (
optionID INT(10) NOT NULL,
itemOptionID INT(10) NOT NULL,
UNIQUE KEY (optionID, itemOptionID)
);
If possible I would use Foreign Keys to link the both columns to the corresponding columns in the tables item_option and option. It works the same with OptionValue and ItemOptionValue.
How to handle the change?
Whenever the controller for the OptionValue change is called, simply update the Item* models as well.
If an Option or OptionValue is deleted, it is up to you, if you delete the ItemOption or ItemOptionValue as well.