Info
I'm having some issues with saving a model that has many MANY_MANY relationships. I have a page where you can add product attributes as well as product attribute levels. What I now want to do is add support for this on the update-page on the product. So when I enter the update-page, I will see all product attributes, and for each product attribute, there will be a drop-down list with the related product attribute levels for that specific product attribute.
Database
Product
id
etc
ProductAttribute
id
etc
ProductAttributeLevel
id
product_attribute_id ## FK
etc
ProductProductAttributeLevel -- This is the pivot-table
product_id ## FK PK
product_attribute_level_id ## FK PK
ActiveRecords
Product:
class Product extends S360ActiveRecord {
public function behaviors() {
return array('CAdvancedArBehavior' => array(
'class' => 'application.extensions.CAdvancedArBehavior')
);
}
public function rules() {
return array(
array('attributeLevels', 'safe'),
);
}
public function relations() {
return array(
'attributeLevels' => array(self::MANY_MANY,
'ProductAttributeLevel',
'product_product_attribute_level(product_id,product_attribute_level_id)'
),
);
}
}
ProductAttribute:
class ProductAttribute extends S360ActiveRecord {
public function relations() {
return array(
'levels' => array(self::HAS_MANY, 'ProductAttributeLevel', 'product_attribute_id'),
);
}
}
ProductAttributeLevel:
class ProductAttributeLevel extends S360ActiveRecord {
public function relations() {
return array(
'productAttribute' => array(self::BELONGS_TO, 'ProductAttribute', 'product_attribute_id'),
'products' => array(self::MANY_MANY, 'Product', 'product_product_attribute_level(product_attribute_level_id,product_id)'),
);
}
}
ProductProductAttributeLevel:
class ProductProductAttributeLevel extends S360ActiveRecord {
public function relations()
{
return array(
'productAttributeLevel' => array(self::BELONGS_TO, 'ProductAttributeLevel', 'product_attribute_level_id'),
'product' => array(self::BELONGS_TO, 'Product', 'product_id'),
);
}
}
My ProductController method that updates a product looks like this:
public function actionUpdate($id) {
$model = $this->loadModel($id);
$this->performAjaxValidation($model);
if (isset($_POST['Product'])) {
$model->attributes = $_POST['Product'];
if ($model->save()) {
$this->redirect(array('index'));
}
}
$this->render('update', array('model' => $model));
}
Relevant part in my form-view:
<?php
$form=$this->beginWidget('S360ActiveForm', array(
'id' => 'product-form',
'enableAjaxValidation' => true,
));
?>
<?php $attributes = ProductAttribute::model()->findAllByAttributes(array('survey_id' => $model->parent_id)); if ($attributes): ?>
<div class="span6">
<?php foreach ($attributes as $attribute) {
echo $form->dropDownListRow($model, 'attributeLevels',
CMap::mergeArray(
array('0' => Yii::t('backend','No attribute level')),
CHtml::listData($attribute->levels, 'id', 'label')
),
array('class' => 'span5')
);
}?>
</div>
<?php endif; ?>
Issue
I get this CDBException:
CDbCommand failed to execute the SQL statement: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (product_product_attribute_level, CONSTRAINT product_product_attribute_level_ibfk_2 FOREIGN KEY (product_attribute_level_id) REFERENCES product_attribute_level (id) ON DELE). The SQL statement executed was: insert into product_product_attribute_level (product_id, product_attribute_level_id) values ('5', '0')
Problem is though that product_attribute_level with id "0" does not exist, the id's starts at "1". How would I change it so that it inserts the correct id-number?
Example of what I want
Let's say I have 2 product attributes; Attribute1 and Attribute2.
Attribute1 have product attribute levels Attribute1_Level1 and Attribute1_Level2.
Attribute2 have product attribute levels Attribute2_Level1, Attribute2_Level2 and Attribute2_Level3.
When I go to my Product edit-/update -page, I want to see this:
Attributes http://img201.imageshack.us/img201/9252/screenshot20130207at103.png
Attribute2 Dropdown http://img405.imageshack.us/img405/9252/screenshot20130207at103.png
The Product belongs to a Survey. The Product Attribute's belongs to a Survey as well so fetching all the Product Attributes that the Product can have is easy:
$attributes = ProductAttribute::model()->findAllByAttributes(array('survey_id' => $product->survey_id));
After this I need to fetch all Product Attribute Levels that belongs to each attribute, which is quite easy as well:
foreach ($attributes as $attribute) {
echo $form->dropDownList($attribute, 'label',
CHtml::listData($attribute->levels, 'id', 'label'),
$htmlOptions
);
}
The problem is how to connect it with the Product and have its "$product->attributeLevels" relationship update accordingly based on what I select from the different dropdowns. $product->attributeLevels should be a list of ProductAttributeLevel and should be stored via the table "product_product_attribute_level".
And of course you are selecting from the dropdown? because if not you are indeed sending a '0'
<?php foreach ($attributes as $attribute) {
echo $form->dropDownListRow($model, 'attributeLevels',
CMap::mergeArray(
// **HERE**
array('0' => Yii::t('backend','No attribute level')),
CHtml::listData($attribute->levels, 'id', 'label')
),
array('class' => 'span5')
);
}?>
If what you want is to have something as the first option that doesn't represents a record, there are two options, use the prompt or the empty attributes of dropDownList, from the docs:
prompt: string, specifies the prompt text shown as the first list option. Its value is empty. Note, the prompt text will NOT be
HTML-encoded.
empty: string, specifies the text corresponding to empty selection. Its value is empty. The 'empty' option can also be an array
of value-label pairs. Each pair will be used to render a list option
at the beginning. Note, the text label will NOT be HTML-encoded.
Now, you want a dropdown list of attributeLevels, but you want them saved on the product. so iterate over the attributes, get its levels, but save them on the product, like this:
<?php foreach ($attributes as $i => $attribute) {
echo $form->dropDownListRow($product, "[$i]attributeLevels",
CHtml::listData($attribute->levels, 'id', 'label'),
array('class' => 'span5', 'prompt' => 'No attribute level')
);
}?>
Now to save them on your product, do this in your controller:
public function actionUpdate($id) {
$model = $this->loadModel($id);
$this->performAjaxValidation($model);
if (isset($_POST['Product'])) {
$attrLevels = $_POST['Product']['attributeLevels'];
unset($_POST['Product']['attributeLevels']);
$model->attributes = $_POST['Product'];
if( $model->save() ) {
$valid=true;
foreach($attrLevels as $i=>$attrLevel)
{
$pivot = new ProductProductAttributeLevel;
$pivot->product_id = $model->id;
$pivot->product_attribute_level_id = $attrLevel;
$valid=$item->save() && $valid;
}
if($valid){
$this->redirect(array('index'));
}
}
}
$this->render('update', array('model' => $model));
}
Disclaimer: copy/paste may not work, but you get the idea
Related
I'm working on laravel e-commerce project where I need to add Attributes to my posts (image below as example)
My question is how to achieve that? should i create new tables or can I add manually from post.create like any other e-commerce cms?
Personally I prefer to be able to add fields in post.create like I
add + button and each time I click on it 2 input fields add and I
can put key and value in it. (if you can help me with that)
Thanks.
Update:
With suggest of #anas-red I've created this structure now:
attributes table.
Schema::create('attributes', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->unique();
$table->timestamps();
});
and product_attributes table
Schema::create('product_attributes', function (Blueprint $table) {
$table->increments('id');
$table->integer('product_id')->unsigned();
$table->foreign('product_id')->references('id')->on('products');
$table->integer('attribute_id')->unsigned();
$table->foreign('attribute_id')->references('id')->on('attributes');
$table->string('attribute_value')->nullable();
$table->timestamps();
});
now i have this store method on my controller when i save my posts:
public function store(Request $request)
{
//Validating title and body field
$this->validate($request, array(
'title'=>'required|max:225',
'slug' =>'required|max:255',
'user_id' =>'required|numeric',
'image_one' =>'nullable|image',
'image_two' =>'nullable|image',
'image_three' =>'nullable|image',
'image_four' =>'nullable|image',
'image_one' =>'nullable|image',
'short_description' => 'nullable|max:1000',
'description' => 'nullable|max:100000',
'subcategory_id' => 'required|numeric',
'discount' => 'nullable|numeric',
'discount_date' => 'nullable|date',
'price' => 'required|numeric',
));
$product = new Product;
$product->title = $request->input('title');
$product->slug = $request->input('slug');
$product->user_id = $request->input('user_id');
$product->description = $request->input('description');
$product->short_description = $request->input('short_description');
$product->subcategory_id = $request->input('subcategory_id');
$product->discount = $request->input('discount');
$product->discount_date = $request->input('discount_date');
$product->price = $request->input('price');
if ($request->hasFile('image')) {
$image = $request->file('image');
$filename = 'product' . '-' . time() . '.' . $image->getClientOriginalExtension();
$location = public_path('images/');
$request->file('image')->move($location, $filename);
$product->image = $filename;
}
$product->save();
$product->attributes()->sync($request->attributes, false);
//Display a successful message upon save
Session::flash('flash_message', 'Product, '. $product->title.' created');
return redirect()->route('admin.products.index');
}
The process i want to do is this:
Store my attributes
Select my attributes while creating new post
Give value to selected attribute
save post_id arribute_id and atteribute_value in product_attributes table.
here is the error i get:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'attributes_id'
in 'field list' (SQL: select attributes_id from product_attributes
where product_id = 29)
UPDATE:
Product model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use jpmurray\LaravelCountdown\Traits\CalculateTimeDiff;
class Product extends Model
{
use CalculateTimeDiff;
protected $table = 'products';
protected $fillable = [
'title', 'slug', 'image_one', 'image_two', 'image_three', 'image_four', 'short_description', 'description', 'price', 'discount', 'discount_date',
];
public function category(){
return $this->belongsTo(Category::class);
}
public function subcategory(){
return $this->belongsTo(Subcategory::class);
}
public function attributes()
{
return $this->belongsToMany(Attribute::class, 'product_attributes', 'product_id', 'attribute_id');
}
public function order(){
return $this->hasMany(Order::class);
}
public function discounts(){
return $this->hasMany(Discount::class, 'product_id', 'id');
}
}
Attribute model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Attribute extends Model
{
protected $fillable = [
'title',
];
public function products(){
return $this->belongsToMany(Product::class);
}
}
I think you can add new table lets say "post_attributes" with the following columns:
id - post_id - key - value
in the PostAttribute model add this:
public function post
{
return $this->belongsTo(Post::class);
}
in the Post model add the following:
public function attributes
{
return $this->hasMany(PostAttributes::class, 'post_attributes');
}
Now the app is flexible enough to handle multiple attributes to one post or a single attribute to another.
Other approach is to implement JSON in your database. Hope that helped you.
update Product model
public function attributes()
{
return $this->belongsToMany(Attribute::class, 'product_attributes', 'product_id', 'attribute_id')->withPivot('attribute_value')->withTimestamps();
}
and update Attribute model to
public function products()
{
return $this->belongsToMany(Product::class, 'product_attributes', 'attribute_id', 'product_id')->withPivot('attribute_value')->withTimestamps();
}
If I see your Product and Attribute Models I will be in a better position to answer you properly.
But any way, I think your problem is with the product_attributes table.
This table is now acting as a pivot (intermediate) table and it is not following Laravel naming convention. The convention is to name it as follows: attribute_product.
Next, you have to add the following into both models i.e. Product and Attribute.
in Attribute Model add:
$this->belongsToMany(Product::class)->withPivot('value');
in Product Model add:
$this->belongsToMany(Attribute::class)->withPivot('value');
To add value to more_value column on pivot table. Use the following:
$product->attributes()->attach($attributeId, ['more_value' => $string]);
or use sync:
$product->attributes()->sync([$attributeId => ['more_value' => $string]]);
lol. the important part is repo code is:
<input type="hidden" id="appOrderItems" name="orderItems[]">
trace appOrderItems in my JS section and you will get it.
in simple words:
when the user adds attributes to a product (in my case, items to an order) then, the appOrderItems array will get the id of the attribute and any additional value that you need to add into the pivot table (other than the product_id and attribute_id. in your case the mores_value). After gathering these attributes into appOrderItems JS array I push its value to the hidden HTML field (name="orderItems[]"). in this case it will be sent to the controller for further process.
I have got two models: Rooms and RoomAttributes. There is a many-many relation between them:
$this->hasManyToMany(
"id",
"RoomAttributes",
"roomID",
"attributesID",
"roomattributesrelation",
"id",
array('alias' => 'attributes')
);
Now I'm creating a form to add a new room and I want to have a list of all attributes as checkboxes. What is the best way to do this and how should I save my room model after?
Maybe something like this:
use Phalcon\Forms\Element\Select;
class RoomForm extends \Phalcon\Forms\Form {
$attr_arr = ['attr1_id' => 'attr1_name', 'N_id' => 'N_name'];
// or $attr_arr= array_column(RoomAttributes::find()->toArray(),'id','name')
$attributes = new Select(
'attributes[]',
$attr_arr ,
['multiple' => 'multiple'
]);
$this->add($attributes);
}
in controller
****
if($new_room->save()){
$attributes = $_POST['attributes'];
foreach ($attributes as $id){
$new_attribute = new RoomAttributes();
$new_attribute->roomID = $new_room->id;
$new_attribute->attributesID = $id;
$new_attribute->save();
}
}
I am adding a mass action to add a category. I am most of the way there I only have one function left to figure out.
Clr\Categorymassaction\controllers\Adminhtml\Catalog\ProductController.php
class Clr_Categorymassaction_Adminhtml_Catalog_ProductController extends Mage_Adminhtml_Controller_Action
{
public function massCategoryAction()
{
$productIds = $this->getRequest()->getParam('product');
$cat = $this->getRequest()->getParam('Category');
if (!is_array($productIds)) {
$this->_getSession()->addError($this->__('Please select product(s).'));
$this->_redirect('*/*/index');
}
else {
$cat = $category['label']->getCategoryId();
foreach($productIds as $product) {
//Process $cat into categoryId append categoryId to $productId
$cat->setPostedProducts($product);
}
//Save product
$cat->save();
}
}
}
Clr\Categorymassaction\Model\Observer
class Clr_Categorymassaction_Model_Observer {
public function addCategoryMassAction(Varien_Event_Observer $observer)
{
$block = $observer ->getBlock();
if ($block instanceof Mage_Adminhtml_Block_Catalog_Product_Grid) {
$block->getMassactionBlock()->addItem('Clr_Categorymassaction', array(
'label' => Mage::helper('catalog')->__('Add to Category'),
'url' => $block->getUrl('*/*/massCategory', array('_current' => true)),
'additional'=> array(
'visibility' => array(
'name' =>'Category',
'class' =>'required-entry',
'label' =>Mage::helper('catalog')->__('Categories'),
'type' => 'select',
'values' => Mage::getModel('Categorymassaction/system_config_source_category')->toOptionArray(),
'renderer' => 'Categorymassaction/catalog_product_grid_render_category',
)
)
));
};
}
}
One last thing
class Clr_Categorymassaction_Model_System_Config_Source_Category
{
public function toOptionArray($addEmpty = true)
{
$options = array();
foreach ($this->load_tree() as $category) {
$options[$category['value']] = $category['label'];
}
return $options;
}
I am mostly in trouble here because I am refactoring, Flagbit_changeattributeset and Vuleticd_AdminGridCategoryFilter. I know what I need to do (at least I think I do) I just don't know how to finish this off. Thanks for your eyes and ears if you read it all.
UPDATE: The observer from Vuleticd_AdminGridCategoryFilter had this additional code
'filter_condition_callback' => array($this, 'filterCallback'),
)
)
));
};
}
public function filterCallback($collection, $column)
{
$value = $column->getFilter()->getValue();
$_category = Mage::getModel('catalog/category')->load($value);
$collection->addCategoryFilter($_category);
return $collection;
}
This was used to apply the filter to the grid. What I am trying to do is instead of using the dropdown to filter column fields; use the dropdown to trigger the ProductController to pass the selected items a new categoryid.
https://magento.stackexchange.com/questions/67234/productcontroller-for-mass-action Asked this question over at magento's stackexchange figured I would post the link here for posterity.
I have two tables, 'users' and 'posts', looking like this:
users:
- id
- username
- password
...
posts:
- id
- user_id (foreign key referencing users.id)
- text
Basically, a user has multiple posts (blog-type posts). Now, I'm trying to create a new post as a logged in user, but I can't get it to work. Here's what I've done:
// 'User' model
class User extends AppModel
{
public $name = 'User';
public $hasMany = array('Post');
...
// 'Post' model
class Post extends AppModel
{
public $name = 'Post';
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
// In PostsController
public function create()
{
if($this->request->is('post'))
{
$this->Post->create();
if($this->Post->save($this->request->data)
{
// Success
}
}
}
// In the post view
<?php echo $this->Session->flash('auth'); ?>
<?php echo $this->Form->create('Post', array('action' => 'create')); ?>
<fieldset>
<legend>
<?php echo __("Write a post"); ?>
</legend>
</fieldset>
<?php echo $this->Form->end(__('Post')); ?>
If I write a post and click 'Post', I get an integrity constraint violation:
Error: SQLSTATE[23000]: Integrity constraint violation:
1452 Cannot add or update a child row: a foreign key
constraint fails (`yams`.`posts`, CONSTRAINT `user_id`
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION)
Am I missing something here? It looks like the user id is not saved to the model.
EDIT:
I forgot to mention, the database error also prints out the SQL query which is clearly wrong:
INSERT INTO `yams`.`posts` (`text`) VALUES ('this is a test post.')
There's no ID whatsoever...
You need to do this:
// In PostsController
public function create()
{
if($this->request->is('post'))
{
$this->request->data['Post']['user_id'] = $this->Auth->user('id');
$this->Post->create();
if($this->Post->save($this->request->data)
{
// Success
}
}
}
I am just copying the book here, i have not used cakePHP at all!
According to the book: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html then a 'hasMany' relationship should look similar to:
class User extends AppModel {
public $hasMany = array(
'Recipe' => array(
'className' => 'Recipe',
'conditions' => array('Recipe.approved' => '1'),
'order' => 'Recipe.created DESC'
)
);
}
You have:
public $hasMany = array('Post');
Should there be mention of a classname in yours?
i.e.
public $hasMany = array(
'Post' => array(
'className' => 'Post'
)
);
With this then the ORM can work out how the classes relate and what to fill in at run time.
I have a one to many relationship where a movie can have many youtubeclips.. Ive managed to create the models and display the movie data inside the update form. I now need to be able to create a loop of some sort to out put my many relationship data into the form.. But cant seem to figure out how to do it.. This is what I have so far..
MovieController --
public function actionUpdate($id)
{
$model=$this->loadModel($id);
$modelYoutubeVideo=$this->loadYoutubeVideoModel($id);
$modelTwitterFeed=$this->loadTwitterModel($id);
if(isset($_POST['Movie']))
{
$model->attributes=$_POST['Movie'];
if($model->save())
$this->redirect(array('view','id'=>$model->id));
}
$this->render('update',array(
'model'=>$model,
'modelYoutubeVideo'=>$modelYoutubeVideo,
'modelTwitterFeed'=> $modelTwitterFeed
));
}
Update Form --
<div class="row clone">
<?php echo $form->labelEx($modelYoutubeVideo,'embed_code'); ?>
<?php echo $form->textField($modelYoutubeVideo,'embed_code[]',array('size'=>50,'maxlength'=>50)); ?>
<?php echo $form->error($modelYoutubeVideo,'embed_code'); ?>
<?php echo $form->labelEx($modelYoutubeVideo,'description'); ?>
<?php echo $form->textField($modelYoutubeVideo,'description[]',array('size'=>50,'maxlength'=>250)); ?>
<?php echo $form->error($modelYoutubeVideo,'description'); ?>
</div>
<?php
$this->widget('ext.widgets.reCopy.ReCopyWidget', array(
'targetClass'=>'clone',
));
?>
Need to Create a loop which outputs my data from the many relations ship table --
<?php
for ($i = 0; $i < count($modelYoutubeVideo); ++$i) {
($modelYoutubeVideo->embed_code[i]);
($modelYoutubeVideo->embed_code[i]);
}
?>
Movie Relationships --
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'competitions' => array(self::HAS_MANY, 'Competition', 'movie_id'),
'studio' => array(self::BELONGS_TO, 'Studio', 'studio_id'),
'country' => array(self::BELONGS_TO, 'Country', 'country_id'),
'movieRating' => array(self::BELONGS_TO, 'MovieRating', 'movie_rating_id'),
'mapPin' => array(self::BELONGS_TO, 'MapPin', 'map_pin_id'),
'twitterFeeds' => array(self::HAS_MANY, 'TwitterFeed', 'movie_id'),
'YoutubeVideo' => array(self::HAS_MANY, 'YoutubeVideo', 'movie_id'),
);
}
in your actionUpdate you can use use relation like this
$model=$this->loadModel($id);
$youTubevideos=$model->YoutubeVideo;
Here YouTubeVideo is the same relation name in your model.It will bring you all the youtubeVideo active records related to specific movie. Now pass this array to your view like
$this->render('update',array(
'model'=>$model,
'modelYoutubeVideo'=>$modelYoutubeVideo,
'modelTwitterFeed'=> $modelTwitterFeed,
'youTubeVideos'=>$youTubeVideos,
));
then in your view use foreach loop to show each value of the model like
foreach($youTubeVideos as $video)
{
echo $video->name;
}
In above echo statement actually you can echo any thing like CdetailView widget or anything you want repeatedly.