How can I correctly relate these tables in cakephp? - php

I'm trying to create a set of CRUDs using cakephp3. My database model looks like this:
I used the cake's tutorial on authentication to create the users table and it's classes, it's working fine. But I want to use a more complex set of roles, so I created these other tables. After creating the database model I baked the corresponding classes, made a few tweaks and got the systems and the roles CRUD's to work. Now I want to integrate the roles_users table, probably inside of user's CRUD.
I would like to see how cake's bake would do it before coding this relation myself, but I'm unable to open /rolesUsers. When I call the URL, I get the following error message:
Cannot match provided foreignKey for "Roles", got "(role_id)" but expected foreign key for "(id, system_id)" RuntimeException
I think it happens because system_id is a PK in roles table and isn't present in roles_users (I'll show the baked models and this PK will be present at roles class). Is there an easy way to make it work without adding system_id in roles_users? IMO adding this extra field wouldn't be a big problem, but I would like to know if I'm doing something wrong, some bad design decision.
My src/Model/Table/RolesUsersTable.php:
<?php
namespace App\Model\Table;
use App\Model\Entity\RolesUser;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* RolesUsers Model
*
* #property \Cake\ORM\Association\BelongsTo $Users
* #property \Cake\ORM\Association\BelongsTo $Roles
*/
class RolesUsersTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('roles_users');
$this->displayField('user_id');
$this->primaryKey(['user_id', 'role_id']);
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Roles', [
'foreignKey' => 'role_id',
'joinType' => 'INNER'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->add('valido_ate', 'valid', ['rule' => 'date'])
->requirePresence('valido_ate', 'create')
->notEmpty('valido_ate');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['user_id'], 'Users'));
$rules->add($rules->existsIn(['role_id'], 'Roles'));
return $rules;
}
}
My src/Model/Table/RolesTable.php:
<?php
namespace App\Model\Table;
use App\Model\Entity\Role;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Roles Model
*
* #property \Cake\ORM\Association\BelongsTo $Systems
*/
class RolesTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('roles');
$this->displayField('name');
$this->primaryKey(['id', 'system_id']);
$this->belongsTo('Systems', [
'foreignKey' => 'system_id',
'joinType' => 'INNER'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->add('id', 'valid', ['rule' => 'numeric'])
->allowEmpty('id', 'create');
$validator
->requirePresence('name', 'create')
->notEmpty('name');
$validator
->add('status', 'valid', ['rule' => 'numeric'])
->requirePresence('status', 'create')
->notEmpty('status');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->existsIn(['system_id'], 'Systems'));
return $rules;
}
}
My src/Model/Table/UsersTable:
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class UsersTable extends Table{
public function validationDefault(Validator $validator){
return $validator
->notEmpty('username', 'O campo nome de usuário é obrigatório')
->notEmpty('password', 'O campo senha é obrigatório')
->notEmpty('role', 'O campo perfil é obrigatório')
->add('role', 'inList', [
'rule' => ['inList', ['admin', 'author']],
'message' => 'Escolha um perfil válido'
]
);
}
}
?>

Answered by user jose_zap in #cakephp #freenode:
In RolesUsersTable.php, initialize function, I added a parameter to both $this->belongsTo calls, including the 'bindingKey' and value 'id'. So this old code:
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Roles', [
'foreignKey' => 'role_id',
'joinType' => 'INNER'
]);
became this:
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'bindingKey' => 'id',
'joinType' => 'INNER'
]);
$this->belongsTo('Roles', [
'foreignKey' => 'role_id',
'bindingKey' => 'id',
'joinType' => 'INNER'
]);

Related

Contain only works when specifying either one of required models

If I add two models in contain, at the same time, only one property will be populated. If I add either one separately, the corresponding property will be populated fine. I suspect a clash with the users table somehow.
Basics:
task lead: tasks hasOne organizational_units hasOne users
tasks [id, organizational_unit_id, ...] > organizational_units [id, user_id, ...] > users [id, ...]
task collaborators: tasks hasAndBelongsToMany users (tasks_users)
tasks [id, ...] > tasks_users [task_id, user_id] > users [id, ...]
Code:
//populates $task->organizational_unit property
$this->Tasks->find()->contain(['OrganizationalUnits.Users'])->where(['Tasks.id' => $id]);
//populates $task->users property
$this->Tasks->find()->contain(['Users'])->where(['Tasks.id' => $id]);
//only populates organizational_unit property, missing users property
$this->Tasks->find()->contain(['OrganizationalUnits.Users','Users'])->where(['Tasks.id' => $id]);
Task Table:
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Tasks Model
*
* #property \App\Model\Table\TasksTable&\Cake\ORM\Association\BelongsTo $ParentTasks
* #property \App\Model\Table\OrganizationalUnitsTable&\Cake\ORM\Association\BelongsTo $OrganizationalUnits
* #property \App\Model\Table\FiscalYearsTable&\Cake\ORM\Association\BelongsTo $FiscalYears
* #property \App\Model\Table\TaskStatusesTable&\Cake\ORM\Association\BelongsTo $TaskStatuses
* #property \App\Model\Table\QuartersTable&\Cake\ORM\Association\BelongsTo $Quarters
* #property \App\Model\Table\TaskTypesTable&\Cake\ORM\Association\BelongsTo $TaskTypes
* #property \App\Model\Table\TaskPrioritiesTable&\Cake\ORM\Association\BelongsTo $TaskPriorities
* #property \App\Model\Table\TaskDiscussionNotesTable&\Cake\ORM\Association\HasMany $TaskDiscussionNotes
* #property \App\Model\Table\TaskOutcomesTable&\Cake\ORM\Association\HasMany $TaskOutcomes
* #property \App\Model\Table\TasksTable&\Cake\ORM\Association\HasMany $ChildTasks
* #property \App\Model\Table\UsersTable&\Cake\ORM\Association\BelongsToMany $Users
*
* #method \App\Model\Entity\Task newEmptyEntity()
* #method \App\Model\Entity\Task newEntity(array $data, array $options = [])
* #method \App\Model\Entity\Task[] newEntities(array $data, array $options = [])
* #method \App\Model\Entity\Task get($primaryKey, $options = [])
* #method \App\Model\Entity\Task findOrCreate($search, ?callable $callback = null, $options = [])
* #method \App\Model\Entity\Task patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* #method \App\Model\Entity\Task[] patchEntities(iterable $entities, array $data, array $options = [])
* #method \App\Model\Entity\Task|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
* #method \App\Model\Entity\Task saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
* #method \App\Model\Entity\Task[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
* #method \App\Model\Entity\Task[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
* #method \App\Model\Entity\Task[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
* #method \App\Model\Entity\Task[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
*
* #mixin \Cake\ORM\Behavior\TimestampBehavior
* #mixin \Cake\ORM\Behavior\TreeBehavior
*/
class TasksTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('tasks');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Tree', [
'level' => 'level', // Defaults to null, i.e. no level saving
]);
$this->belongsTo('ParentTasks', [
'className' => 'Tasks',
'foreignKey' => 'parent_id',
]);
$this->belongsTo('OrganizationalUnits', [
'foreignKey' => 'organizational_unit_id',
'joinType' => 'INNER',
]);
$this->belongsTo('FiscalYears', [
'foreignKey' => 'fiscal_year_id',
'joinType' => 'INNER',
]);
$this->belongsTo('TaskStatuses', [
'foreignKey' => 'task_status_id',
]);
$this->belongsTo('Quarters', [
'foreignKey' => 'quarter_id',
]);
$this->belongsTo('TaskTypes', [
'foreignKey' => 'task_type_id',
'joinType' => 'INNER',
]);
$this->belongsTo('TaskPriorities', [
'foreignKey' => 'task_priority_id',
'joinType' => 'INNER',
]);
$this->hasMany('TaskDiscussionNotes', [
'foreignKey' => 'task_id',
]);
$this->hasMany('TaskOutcomes', [
'foreignKey' => 'task_id',
]);
$this->hasMany('ChildTasks', [
'className' => 'Tasks',
'foreignKey' => 'parent_id',
]);
$this->belongsToMany('Users', [
'foreignKey' => 'task_id',
'targetForeignKey' => 'user_id',
'joinTable' => 'tasks_users',
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator): \Cake\Validation\Validator
{
//removed
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules): \Cake\ORM\RulesChecker
{
//removed
return $rules;
}
}

My storage fake doesn't store the image inside the test directory

I'm writing a test for my Item model, it has images to be uploaded and I expect it should be stored inside the testing directory to be cleared each time I ran the test. But it wasn't. Instead, it creates a new directory named item-images in both storage and testing/disks but it uses the actual storage/item-images directory instead of storage/disks/item-images.
Storage::fake('item-images') is already defined on top of the test case, I'm a bit confused by this, please help.
tests/Feature/ItemTest.php
public function test_user_can_store_new_item()
{
Storage::fake('item-images');
Sanctum::actingAs(User::factory()->create());
$images = [
UploadedFile::fake()->image('image1.png'),
UploadedFile::fake()->image('image2.png'),
UploadedFile::fake()->image('image3.png'),
];
$item = Item::factory()->make(['images' => $images]);
$this->postJson(route('items.store'), $item->toArray())
->dump()
->assertCreated()
->assertJsonFragment($item->toArray());
foreach ($images as $image) {
Storage::disk('item-images')->assertExists($image->hashName());
Storage::disk('item-images')->assertMissing('missing.png');
}
}
app/Models/Item.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Item extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'category_id',
'name',
'description',
'history',
];
protected $with = ['category'];
/**
* Get the category that owns the Item
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
app/Http/Controllers/ItemController.php
/**
* Store a newly created resource in storage.
*
* #param \App\Http\Requests\RequestItem $request
* #return \Illuminate\Http\Response
*/
public function store(RequestItem $request)
{
$data = $request->validated();
$item = Item::create(Arr::except($data, 'images'));
if ($request->has('images')) {
foreach ($request->file('images') as $image) {
$path = $image->store('item-images');
Image::create(['item_id' => $item->id, 'url' => $path]);
}
}
return response()->json($item, 201);
}
app/Http/Requests/RequestItem.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RequestItem extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'category_id' => [
'required',
'integer',
],
'name' => [
'required',
'string',
'max:255',
Rule::unique('items', 'name'),
],
'description' => [
'required',
'string',
'max:255',
],
'history' => [
'string',
'max:2500',
],
'images' => [
'required',
'array',
'min:1',
'max:10',
],
];
}
}
I believe the confusing is you are creating a fake file, but actually properly saving it. $image->store('item-images'); doesn't specify the store you are saving it in, but the 'item-images' is the path and it will save it to the default storage. So either Storage::fake() the default storage option.
// assuming default storage is local
Storage::fake('local');
Or change your logic to specify the correct faked storage disk.
Storage::disk('item-images')->putFile('path/yourfile', $image);

Model Related Fields - Update and Create using FK

I have two tables. One for Users, one for Equipments.
I managed to make the search for username from other table, but now i want to create an equipment or to update an equipment directly to username, not by user_for_id. Is there any posibilty to do that ? Which would be the steps ? Thank you very much for any guidance.
First table : user_id - primary key, username, password ;
Second table: user_equip_id - primary key, phone_model, phone_model_series, phone_date_acq, nb_sg_model, nb_sg,_date_acq, display_model, display_series, display_date_acq, user_for_id - foreign key to user_id from table1
Controller:
<?php
namespace app\controllers;
use Yii;
use app\models\Equipment;
use app\models\EquipmentSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii\filters\AccessControl;
/**
* EquipmentController implements the CRUD actions for Equipment model.
*/
class EquipmentController extends Controller
{
/**
* {#inheritdoc}
*/
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'delete' => ['POST'],
],
],
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['#'],
],
],
],
];
}
/**
* Lists all Equipment models.
* #return mixed
*/
public function actionIndex()
{
$searchModel = new EquipmentSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single Equipment model.
* #param integer $id
* #return mixed
* #throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
/**
* Creates a new Equipment model.
* If creation is successful, the browser will be redirected to the 'view' page.
* #return mixed
*/
public function actionCreate()
{
$model = new Equipment();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->user_equip_id]);
}
return $this->render('create', [
'model' => $model,
]);
}
/**
* Updates an existing Equipment model.
* If update is successful, the browser will be redirected to the 'view' page.
* #param integer $id
* #return mixed
* #throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->user_equip_id]);
}
return $this->render('update', [
'model' => $model,
]);
}
/**
* Deletes an existing Equipment model.
* If deletion is successful, the browser will be redirected to the 'index' page.
* #param integer $id
* #return mixed
* #throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();
return $this->redirect(['index']);
}
/**
* Finds the Equipment model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* #param integer $id
* #return Equipment the loaded model
* #throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = Equipment::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}
Model :
<?php
namespace app\models;
use Yii;
/**
* This is the model class for table "equipment".
*
* #property int $user_equip_id
* #property string|null $phone_model
* #property string $phone_series
* #property string $phone_date_acq
* #property string $nb_sg_model
* #property string $nb_sg_series
* #property string $nb_sg_date_acq
* #property string $display_model
* #property string $display_series
* #property string $display_date_acq
* #property int $user_for_id
*
* #property User $userFor
*/
class Equipment extends \yii\db\ActiveRecord
{
/**
* {#inheritdoc}
*/
public static function tableName()
{
return 'equipment';
}
/**
* {#inheritdoc}
*/
public function rules()
{
return [
[['phone_series', 'phone_date_acq', 'nb_sg_model', 'nb_sg_series', 'nb_sg_date_acq', 'display_model', 'display_series', 'display_date_acq', 'user_for_id'], 'required'],
[['phone_date_acq', 'nb_sg_date_acq', 'display_date_acq'], 'safe'],
[['user_for_id'], 'integer'],
[['userFor'], 'safe'],
[['phone_model', 'phone_series', 'nb_sg_model', 'nb_sg_series', 'display_model', 'display_series'], 'string', 'max' => 50],
[['user_for_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['user_for_id' => 'user_id']],
[['userFor'], 'string', 'max' => 50],
//[['username'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['username' => 'username']],
];
}
/**
* {#inheritdoc}
*/
public function attributeLabels()
{
return [
'user_equip_id' => 'User Equip ID',
'phone_model' => 'Phone Model',
'phone_series' => 'Phone Series',
'phone_date_acq' => 'Phone Date',
'nb_sg_model' => 'NB/GS Model',
'nb_sg_series' => 'NB/GS Series',
'nb_sg_date_acq' => 'NB/GS Date',
'display_model' => 'Display Model',
'display_series' => 'Display Series',
'display_date_acq' => 'Display Date',
'user_for_id' => 'User For ID',
'userFor.username' => 'Username',
];
}
/**
* Gets query for [[UserFor]].
*
* #return \yii\db\ActiveQuery|yii\db\ActiveQuery
*/
public function getUserFor()
{
return $this->hasOne(User::className(), ['user_id' => 'user_for_id']);
}
//public function getUsername()
//{
// print_r($equipment->userFor->username);die;
// }
/**
* {#inheritdoc}
* #return EquipmentQuery the active query used by this AR class.
*/
public static function find()
{
return new EquipmentQuery(get_called_class());
}
}
Model Search:
<?php
namespace app\models;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use app\models\Equipment;
/**
* EquipmentSearch represents the model behind the search form of `app\models\Equipment`.
*/
class EquipmentSearch extends Equipment
{
public $userFor;
/**
* {#inheritdoc}
*/
public function rules()
{
return [
[['user_equip_id', 'user_for_id'], 'integer'],
[['phone_model', 'phone_series', 'phone_date_acq', 'nb_sg_model', 'nb_sg_series', 'nb_sg_date_acq', 'display_model', 'display_series', 'display_date_acq'], 'safe'],
[['userFor'], 'safe'],
#[['username'], 'safe'],
];
}
/**
* {#inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* #param array $params
*
* #return ActiveDataProvider
*/
public function search($params)
{
$query = Equipment::find();
$query->joinWith(['userFor']);
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['username'] = [
'asc' => ['user.username' => SORT_ASC],
'desc' => ['user.username' => SORT_DESC],
];
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'user_equip_id' => $this->user_equip_id,
'phone_date_acq' => $this->phone_date_acq,
'nb_sg_date_acq' => $this->nb_sg_date_acq,
'display_date_acq' => $this->display_date_acq,
'user_for_id' => $this->user_for_id,
]);
$query->andFilterWhere(['like', 'phone_model', $this->phone_model])
->andFilterWhere(['like', 'phone_series', $this->phone_series])
->andFilterWhere(['like', 'nb_sg_model', $this->nb_sg_model])
->andFilterWhere(['like', 'nb_sg_series', $this->nb_sg_series])
->andFilterWhere(['like', 'display_model', $this->display_model])
->andFilterWhere(['like', 'display_series', $this->display_series])
->andFilterWhere(['like', 'user.username', $this->userFor]);
return $dataProvider;
}
}
Thank you in advance for any guidance or help.

Cakephp - Select2

Can someone help me with multiple select in CakePHP 3?
I have a project in CakePHP 3.6. In one of the view, I need to add student's presences, and I wanna do it with a multiple select (Select2).
In one lesson I can choose many students. The students are saved in DB, and in the select input, I wanna get the students from there (DB).
I try to use select2.org script, but it does not work as I want. The input doesn't show the student's name.
This is my input:
echo $this->Form->input('students_id', [
'type' => 'select',
'multiple' => true,
'options' => $students,
'hiddenField' => false,
'id' => 'students_id',
]);
This my script:
$('#students_id').select2({
tag: true,
multiple: 'multiple'
});
And in the controller I get students list in this way:
$students = $this->Students->find('list', ['limit' => 200]);
EDIT:
StudentsTable:
class StudentsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('students');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->belongsTo('Courses', [
'foreignKey' => 'course_id',
'joinType' => 'INNER'
]);
$this->hasMany('LessonPresences', [
'foreignKey' => 'student_id'
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->scalar('name')
->maxLength('name', 45)
->requirePresence('name', 'create')
->notEmpty('name');
$validator
->scalar('lastname')
->maxLength('lastname', 45)
->requirePresence('lastname', 'create')
->notEmpty('lastname');
$validator
->email('email')
->allowEmpty('email');
$validator
->scalar('phone')
->maxLength('phone', 18)
->allowEmpty('phone');
$validator
->integer('grander')
->allowEmpty('grander');
$validator
->scalar('address')
->maxLength('address', 100)
->allowEmpty('address');
$validator
->dateTime('add_date')
->requirePresence('add_date', 'create')
->notEmpty('add_date');
$validator
->date('sign_date')
->requirePresence('sign_date', 'create')
->notEmpty('sign_date');
$validator
->integer('status')
->allowEmpty('status');
$validator
->date('course_end')
->allowEmpty('course_end');
return $validator;
}
}

In CakePHP, how to have a Table belongsToMany OtherTable hasMany AnotherTable

I am creating a tool that allows to generate dynamic forms. There are several tables in question:
Form [the Form master table]
FormField [JoinTable to Field]
Field [the fields available for inclusion in Form]
FieldValidation [The table containing relation data between the FormField and the Validation option]
Validation [The available Validation options]
For the FieldValidation - this could in effect be a hasMany from Field, but I am unsure of whether I need to set up this relation from the Field table, or from the join table FieldValidation. The Validation table literally just includes the definitions for the validation options. This does not actually need to be a belongsToMany relation from the FormField/Field table. A hasMany is fine if that simplifies things.
Is this even possible?
Form -> [FormField] -> Field -> [FieldValidation] -> Validation
I have never done this before - so if there is a better way to approach this, I am all ears. My main concern is being able to select Form, contain Field's, and then contain the Validation for each field selected. Obviously, multiple validation rules can be selected per field.
A little late, but I did resolve this issue.
ERD Diagram of Physical Relationship in DB
Model: UsersTable
class UsersTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('users');
$this->displayField('username');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsToMany('Projects', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'project_id',
'through' => 'ProjectsUsers'
]);
$this->hasMany('ProjectsUsers', [
'foreignKey' => 'user_id'
]);
}
}
Model: ProjectsTable
class ProjectsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('projects');
$this->displayField('name');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsToMany('Users', [
'foreignKey' => 'project_id',
'targetForeignKey' => 'user_id',
'through' => 'ProjectsUsers'
]);
$this->hasMany('ProjectsUsers', [
'foreignKey' => 'project_id'
]);
}
}
Model: ProjectsUsersTable - this is the model for the JOIN table (through)
class ProjectsUsersTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('projects_users');
$this->displayField('id');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('Users', [
'foreignKey' => 'user_id'
]);
$this->belongsTo('Projects', [
'foreignKey' => 'project_id'
]);
$this->hasMany('ProjectsUsersPermissions', [
'foreignKey' => 'projects_users_id'
]);
}
}
Model: ProjectsUsersPermissions - this is the relation to the join table
class ProjectsUsersPermissionsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('projects_users_permissions');
$this->displayField('role');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('ProjectsUsers', [
'foreignKey' => 'projects_users_id'
]);
}
}
Then the controller find action
$this->Projects->find()
->where(
[
'Projects.id' => $projectId
]
)
->contain(
[
'Users', // through belongsToMany
'ProjectsUsers' => [ // through hasMany [joinTableModel]
'ProjectsUsersPermissions' // through hasMany
]
]
)
->first();
This may be overkill for this scenario, and it is not my exact implementation - so don't think I am just doing unnecessary joins/contains. In my real life scenario, this works perfectly.
Hope this helps someone!

Categories