Many to many relationships in Yii2 - yii2

I am trying to create a many to many relationship between a profile and several fields within that profile including languages and specialities. I have looked at several implementations and understand that there are several extensions, but I am required to minimise usage of extensions.
I have created the proper migration...this is a skeleton and the user table is purely for logins and OAuth...so keys can be ignored. As you can see in my controller I don't really know the way forward at this point. My language model is for all intensive purposes static from this controller(It is controlled by an admin backend). What is working? If I make a couple modifications to the below code then the checkboxlist will display with the proper checked items that I manually added to the lookup table. Trying to modify the lookup table from code, I have been unable to do unless I populate the $languageModel with a findOne(knownPK), however that is not usable, because multiple checkboxes can be selected resulting in an array and the link command requires ActiveRecordInterface which is singular. Ideally I would like to simply use
$languageModel->load(Yii::$app->request->post(),$trainerModel->formName());
but that isn't working.
Additionally, is there a mechanism within the framework to remove lookups or is that done manually. Any help or insight would be helpful. Thank you in advance.
public function safeUp()
{
$this->createTable('student', [
'id' => $this->primaryKey(),
'user_id' => $this->integer()->notNull(),
]);
$this->createTable('language', [
'id' => $this->primaryKey(),
'language' => $this->string(63),
]);
$this->createTable('student_language',[
'id' => $this->primaryKey(),
'language_id' => $this->integer(),
'student_id' => $this->integer()
]);
$this->addForeignKey('fk-student-language-language', 'student_language', 'language_id', 'language', 'id', 'CASCADE', 'CASCADE');
$this->addForeignKey('fk-student-language-student', 'student_language', 'student_id', 'student', 'id', 'CASCADE', 'CASCADE');
$this->addForeignKey('fk-student-user-user_id', 'student', 'user_id', 'user', 'id', 'CASCADE', 'CASCADE');
}
My student Active Record
/**
* #return \yii\db\ActiveQuery
*/
public function getstudentLanguages()
{
return $this->hasMany(studentLanguage::className(), ['student_id' => 'id']);
}
public function getLanguages(){
return $this->hasMany(Language::className(),['id'=>'language_id'])->viaTable('student_language',['student_id'=>'id']);
}
My Language Model
/**
* #return \yii\db\ActiveQuery
*/
public function getstudentLanguages()
{
return $this->hasMany(studentLanguage::className(), ['language_id' => 'id']);
}
public function getLanguages(){
return $this->hasMany(student::className(),['id'=>'student_id'])->viaTable('student_language',['language_id'=>'id']);
}
My Controller
public function actionProfile()
{
if (Yii::$app->user->isGuest) {
throw new UnauthorizedHttpException('This page requires you to be logged in');
} else {
$user_id = Yii::$app->user->identity->getId();
$studentModel = $this->findstudentModelByUserId($user_id);
if (is_null($studentModel)) {
$studentModel = new student();
$studentModel->setAttribute('user_id', $user_id);
$studentModel->save();
}
$languageModel = ??????????????????
$studentModel->load(Yii::$app->request->post()) && $studentModel->save() && $studentModel->link('languages',$languageModel);
}
return $this->render('profile', ['model' => $studentModel]);
}
My View
<?= $form->field($model, 'languages')->checkboxList(\common\models\Language::find()->select(
['language', 'id'])->indexBy('id')->column(), ['prompt' => 'select Language']); ?>

This is my solution at this point, however I am interested to hear improvements.
Note: There is no error checking etc...just functional code.
I added this to the student activerecord class
public function linkMultiple( $name,Array $models){
studentLanguage::deleteAll(['student_id' =>$this->id]);
foreach($models as $model){
$this->link($name, $model);
}
return true;
}
Modified my Controller action to the following
public function actionProfile()
{
if (Yii::$app->user->isGuest) {
throw new UnauthorizedHttpException('This page requires you to be logged in');
} else {
$user_id = Yii::$app->user->identity->getId();
$studentModel = $this->findstudentModelByUserId($user_id);
if (is_null($studentModel)) {
$studentModel = new student();
$studentModel->setAttribute('user_id', $user_id);
$studentModel->save();
}
$languages = Yii::$app->request->post('student')['languages'];
$languageModels = Language::findAll($languages);
$studentModel->load(Yii::$app->request->post()) && $studentModel->save() && $studentModel->linkMultiple('languages',$languageModels);
}
return $this->render('profile', ['model' => $studentModel]);
}

Related

Define multiple scenarios and validate with multiple scenarios in Yii 2 model

In model I have defined multiple scenarios:
public function rules() {
return [
[['in_quantity'], 'required','on'=>['stockIn']],
[['out_quantity'], 'required','on'=>['stockOut']],
];
}
Is it possible to use both scenario stockIn and stockOut for single model validation?
$StockModel->scenario[] = 'stockOut';
$StockModel->scenario[] = 'stockIn';
or
$StockModel->scenario = ['stockOut','stockIn'];
You can't have multiple scenarios for model. But you can have multiple scenarios for rule:
public function rules() {
return [
[['in_quantity'], 'required', 'on' => ['stockIn', 'stockOut']],
[['out_quantity'], 'required', 'on' => ['stockIn', 'stockOut']],
];
}
If you need multiple scenarios for model, it means that you're overusing scenarios feature.
Also note that it is not recommended to use too many scenarios in one model - scenarios work fine for simple cases, but more complicated cases should be handled by separate models for each scenario.
You can create multiple scenarios this way in model
class MyModel extends \yii\db\ActiveRecord {
const SCENARIO_CREATE = 'scenario_create';
const SCENARIO_UPDATE = 'scenario_update';
// get scenarios
public function scenarios()
{
return [
self::SCENARIO_CREATE => ['user_id', 'name', 'desc', 'published','date_create'],
self::SCENARIO_UPDATE => ['user_id', 'name', 'desc', 'date_update'],
];
}
public function rules()
{
[['user_id'], 'integer'],
[['name','desc'], 'string', 'max' => 70],
[['date_create', 'date_update'], 'date', 'format' => 'php:Y-m-d H:i:s'],
];
}
}
and you can use this way anywhere
public function actionIndex() {
$model = new MyModel;
$model->scenario = MyModel::SCENARIO_CREATE;
if ($model->load(\Yii::$app->request->post())){
if($model->save()){
// some operations
}
}
}
You could if you extend the rule with when for server validation:
[
['in_quantity'],
'required',
'when' => function ($model) {
return $model->scenario === 'stockIn' || $model->scenario === 'stockOut';
}
]
Also if you want to validate in the form (aka client side validation) you could also use the whenClient that expect a js function:
'whenClient' => "function (attribute, value) {
const scenario = $('#stock-scenario').val()
return scenario === 'stockIn' || scenario = 'stockOut';
}"

OctoberCMS: How to maintain a two-way friendship relation?

I'm extending the rainlab.user plugin to allow each user to have friends via a simple intermediate table with the following fields:
user_id
friend_id
status
I've extended the User model:
use RainLab\User\Models\User as FrontUser;
FrontUser::extend(function($model) {
$model->belongsToMany['friends']=[
'RainLab\User\Models\User',
'table' => 'meysam_social_friends',
'pivot' => ['status'],
'pivotModel' => 'Meysam\Social\Models\FriendsPivot',
'timestamps' => true,
'key' => 'user_id',
'otherKey' => 'friend_id'
];
$model->addDynamicMethod('isFriendWith', function (FrontUser $user) use ($model) {
$model->friends->contains($user->id);
});
$model->addDynamicMethod('addFriend', function (FrontUser $user) use ($model) {
$model->friends()->attach($user->id);
});
$model->addDynamicMethod('removeFriend', function (FrontUser $user) use ($model) {
$model->friends()->detach($user->id);
});
});
And also extended the Rainlab.User Controller to have Friends tab where all friends of a user are listed and can be added and removed:
use RainLab\User\Controllers\Users as UsersController;
UsersController::extend(function($controller) {
if(!isset($controller->implement['Backend.Behaviors.RelationController'])) {
$controller->implement[] = 'Backend.Behaviors.RelationController';
}
$controller->relationConfig = '$/meysam/social/controllers/user/config_relations.yaml';
});
UsersController::extendFormFields(function($form, $model, $context) {
if(!$model instanceof FrontUser or $context != 'preview'){
// friends tab should not be displayed in update and create contexts
return;
}
$form->addTabFields([
'friends' => [
'label' => '',
'tab' => 'Friends',
'type' => 'partial',
'path' => '$/meysam/social/controllers/user/_friends.htm',
]
]);
});
Now I need to maintain a two-way friendship relationship. i.e. whenever user_id and friend_id is added to the friends table, I want to automatically add friend_id and user_id to the table as well. To achieve this, I implemented afterSave and beforeSave in the FriendsPivot model:
class FriendsPivot extends Pivot
{
/*
* Validation
*/
public $rules = [
'status' => 'required'
];
public $belongsTo = [
'user' => ['RainLab\User\Models\User', 'key' => 'user_id'],
'friend' => ['RainLab\User\Models\User', 'key' => 'friend_id']
];
public function getStatusOptions()
{
return [
1 => 'Pending',
2 => 'Approved',
3 => 'Blocked',
];
}
public function afterSave()
{
Log::info('Saving pivot...');
if(!$this->friend->isFriendWith($this->user)) {
$this->friend->addFriend($this->user);
}
}
public function beforeDelete()
{
Log::info('Deleting pivot...');
if($this->friend->isFriendWith($this->user)) {
$this->friend->removeFriend($this->user);
}
}
}
The problem is that beforeDelete is never called. afterSave gets called but beforeDelete never gets called and therefor the inverse of the relationship is not deleted (user_id-friend_id gets removed from database but friend_id-user_id does not get deleted). Why is beforeDelete not called? Is there anything I'm doing wrong? Is there any better way to maintain a two-way friendship relation?
I found this post because I'm trying to do exactly the same thing as you. If you have solved this then I wonder if you would be willing to share your solution?
This sounds very odd at first, but maybe this is because of the special delete behavior of the Pivot model. It appears that it builds a raw query using the QueryBuilder and thus bypasses any regular Eloquent (October) events.
In my eyes, the best solution would be to trigger the delete event manually in the delete method, but I'm unsure if this has any side effects.
Maybe you could test that and prepare a PR on Github if it works.

How to insert same data record to multiple table on Yii2

I'm using yii2-advanced. I've several table :
tb_user:(iduser(PK),username),
tb_profile:(id,iduser(FK)),
tb_status:(id,iduser(FK))
My question is how can i insert iduser(PK) from tb_user to iduser(FK) on tb_profile and tb_status after i push the signup button.
For a while i think i must to do some modification of bevahiours() function on User model and i found some error, or adding trigger syntax on the table ? (i think this is not a good ways).
Is there anyone who can help me, how to solve my problem ?
this is the User model before the modification :
<?php
namespace common\models;
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\web\IdentityInterface;
class User extends ActiveRecord implements IdentityInterface
{
const STATUS_DELETED = 0;
const STATUS_ACTIVE = 10;
/**
* #inheritdoc
*/
public static function tableName()
{
return '{{%user}}';
}
/**
* #inheritdoc
*/
public function behaviors()
{
return [
'timestamp' => [
'class' => TimestampBehavior::className(),
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => 'created_at',
ActiveRecord::EVENT_BEFORE_UPDATE => 'updated_at',
],
'value' => function () {return date('Y-m-d h:m:s');},
],
];
}
/**
* #inheritdoc
*/
public function rules()
{
return [
['status', 'default', 'value' => self::STATUS_ACTIVE],
['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
];
}
/**
* #inheritdoc
*/
public static function findIdentity($id)
{
return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]);
}
/**
* #inheritdoc
*/
public function getId()
{
return $this->getPrimaryKey();
}
}
?>
The Controller :
public function actionSignup()
{
$model = new SignupForm();
if ($model->load(Yii::$app->request->post())) {
if ($user = $model->signup()) {
if (Yii::$app->getUser()->login($user)) {
return $this->goHome();
}
}
}
return $this->render('signup', [
'model' => $model,
]);
}
I had similar situation in one of my project where i had 2 tables like user,user_image where user_id was foreign key to add the path.
For those kind of situation you can use either of following approach
1.Insert record in both table on click of signup button. You will have to write update action accordingly.
$user = new User();
$user->name = "John"
$user->email = "John#gmail.com"
//Add if any other fields in table
$user->save(); //save the record
$user_image = new UserImage();
$user_image->user_id = $user->id;
$user_image->image = "image path"
//Add any other images here
$user_image->save();//save the record
2.You can also call create action of UserImage and do the same. If you use this approach than you might also need to use any other unique column to find the id of that user and use it to insert new record,for example in my table email is unique column so i can write following code in UserImage and get the id
$user = User::findOne(['email' => 'john#gmail.com']);//this will return whole row
$user_image->user_id = $user->id;
$user_image->image = "image path"
//Add any other images here
$user_image->save();//save the record
And that way you can use the code as per it suits your need
Thank you

Yii2 - insert relational data with junction table, many-many connection

I have a problem with Yii2 (Stable).
I have a Content(PK:id) table, I have a Tag(PK:id) table, and I have a junction table called Content_Tag (PK:content_id, tag_id). I'd like to use it for tagging, like WP tags.
All controllers and models are created with gii.
I have two problems:
If I create a new content, I'd like to save some new tags to the Tag table via the Content_Tag table. How can I do that? With link()?
What if there are tags (I know the ids) in the tag table, I'd like to connect only with the Content table via the junction table, without inserting into the Tag table. How can I do this?
I don't want to write native SQL command, I'd like to use the Yii2 built in functions like link() or via() or viaTable().
Thanks for your help!
I created a behavior to help handle do this, basically you do:
$content = Content::findOne(1);
$tags = [Tag::findOne(2), Tag::findOne(3)];
$content->linkAll('tags', $tags, [], true, true);
You can get the behavior here:
https://github.com/cornernote/yii2-linkall
If you'd prefer to do it without the behavior, something like this:
// get the content model
$content = Content::findOne(1);
// get the new tags
$newTags = [Tag::findOne(2), Tag::findOne(3)];
// get the IDs of the new tags
$newTagIds = ArrayHelper::map($newTags, 'id', 'id');
// get the old tags
$oldTags = $post->tags;
// get the IDs of the old tags
$oldTagIds = ArrayHelper::map($oldTags, 'id', 'id');
// remove old tags
foreach ($oldTags as $oldTag) {
if (!in_array($oldTag->id, $newTagIds)) {
$content->unlink('tags', $oldTag, true);
}
}
// add new tags
foreach ($newTags as $newTag) {
if (!in_array($newTag->id, $oldTagIds)) {
$content->link('tags', $newTag);
}
}
If you created models using gii then you might seen in the model the relationship is done like:
/**
* #return \yii\db\ActiveQuery
*/
public function getContent()
{
return $this->hasMany(Content_Tag::className(), ['content_id' => 'id']);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getContent()
{
return $this->hasMany(Tag::className(), ['tag_id' => 'tag_id'])->viaTable('content_tag', ['content_id' => 'id']);
}
If you want to save in Content_Tag table based on Content and Tag table then in controller you can use:
public function actionCreate()
{
$model = new Tag();
$content = new Content();
$content_tag = new Content_tag();
if($model->load(Yii::$app->request->post()) && $model->save()){
$model->save(false);
$content_tag->tag_id = $model->id;
$content_tag->content_id = $model->content_id;
$content_tag->save(false);
if($model->save(false))
{
Yii::$app->getSession()->setFlash('success', 'Created successfully');
return $this->render('create',[
'model' => $model,
'content' => $content,
'content_tag' => $content_tag
]);
}
}
else
{
return $this->render('create', [
'model' => $model,
]);
}
}
You can use link() to save. I am also searching for that as I didn't use that.

QueryException - Integrity constraint violation: 1062 Duplicate entry when I hit the route 'logout'

I get the above error when I try and logout by hitting the route /logout. The table that is referenced in the screenshot is mdbids. It stores all of my IDs (strings, 16 characters in length).
When a user is created their MDBID (id) is stored in the mdbids table.
routes.php
<?php
Route::get('login', ['as' => 'login', 'uses' => 'SessionsController#create']);
Route::get('logout', ['as' => 'logout', 'uses' => 'SessionsController#destroy']);
SessionsController.php
<?php
use MDB\Forms\LoginForm;
class SessionsController extends \BaseController {
protected $loginForm;
function __construct(LoginForm $loginForm)
{
$this->loginForm = $loginForm;
}
public function create()
{
if(Auth::check()) return Redirect::to("/users");
return View::make('sessions.create');
}
public function store()
{
$this->loginForm->validate($input = Input::only('email','password'));
if (Auth::attempt($input)) {
Notification::success('You signed in successfully!');
return Redirect::intended('/');
}
Notification::error('The form contains some errors');
return Redirect::to('login')->withInput()->withFlashMessage("The form contains some errors");
}
public function destroy()
{
Auth::logout();
return Redirect::home();
}
}
The following is taken from my User.php (model) file. It isn't the whole file as it is fairly big, but this is the only part where IDs are mentioned.
User.php (model)
<?php
public function save(array $options = array())
{
$this->mdbid = $this->mdbid ?: str_random(16);
$this->key = $this->key ?: str_random(11);
Mdbid::create([
'mdbid' => $this->mdbid,
'table_number' => 7,
'table_name' => 'users',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
parent::save($options);
}
I don't know where to start to look. Any help is greatly appreciated.
Your issue is that the logout is actually causing save() to run, and therefor you are causing Mdbid::create to run with an already added key (presumably when you logged in, or somewhere else in your User model?).
Solution #1:
You could add a logout() function to the User model that you have. Something similar to
function logout()
{
$this->mdbid = null;
return Auth::logout()
}
This will stop two of the same keys being added to the logout function.
Solution #2
If what you are trying to accomplish is adding a row upon a successful login, then you should not be using the User::save() function, rather, you should be listening for the auth.login event.
Inside app/start/global.php, add the following code:
Event::listen('auth.login', function($user)
{
$user->mdbid = $user->mdbid ?: str_random(16);
$user->key = $user->key ?: str_random(11);
Mdbid::create([
'mdbid' => $user->mdbid,
'table_number' => 7,
'table_name' => 'users',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
});
This will ensure only one row gets added to Mdbid per successful login, instead of adding a new row (with the same id) each time the User model is updated.
Solution #3 (a.k.a. what was really wanted)
Each table has mdbid as a primary key. Each primary key needs to be added to the Mdbid table each time a new row is inserted.
The way that this should be done is with an Observer. The first part is adding a new Observer class that will be used for all of the models we want to add the mdbid into:
class MdbidObserver
{
/**
* Observe new rows being added into the database
*/
public function creating($model)
{
// note that $model could be any model
$model->mdbid = $model->mdbid ?: str_random(16);
$model->key = $model->key ?: str_random(11);
Mdbid::create([
'mdbid' => $model->mdbid,
'table_number' => 7,
'table_name' => 'users',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
}
}
The second part is adding the Observer to all the models that we want the mdbid added to (inside app/start/global.php):
User::observe(new MdbidObserver);
Artist::observe(new MdbidObserver);
Album::observe(new MdbidObserver);
To stop any issues with mdbid not actually being random already being used, you might want to add a loop just before $model->mdbid. Something similar to:
$isUnique = false;
while (!$isUnique)
{
$unqiueId = str_random(16);
$row = Mdbid::where('mdbid', $uniqueId);
if (is_object($row))
$isUnique = true;
}
$model->mdbid = $uniqueId;