I have a HABTM relation like : Post <-> Tag (a Post can have multiple Tag, and same the other way).
This work with the multiple checkbox selection generated by Cakephp. But I want to have at least one Tag for every Post and throw an error if someone try to insert an orphan.
I'm looking for the cleanest/most CakePHP alike way to do this.
This is more or less an update of this HABTM form validation in CakePHP question, as I get the same problem on my cakephp 2.7 (last cakephp 2.x for now with php 5.3 support at the date of 2016) and can't find a good way to do it.
Here are what I think is the best for now. It use the cakephp 3.x behaviour for HABTM validation.
I choose to only work in model, with the most generic code.
In your AppModel.php, set this beforeValidate() and afterValidate()
class AppModel extends Model {
/** #var array set the behaviour to `Containable` */
public $actsAs = array('Containable');
/**
* copy the HABTM post value in the data validation scope
* from data[distantModel][distantModel] to data[model][distantModel]
* #return bool true
*/
public function beforeValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$model][$model]))
$this->data[$this->name][$model] = $this->data[$model][$model];
}
return true;
}
/**
* delete the HABTM value of the data validation scope (undo beforeValidate())
* and add the error returned by main model in the distant HABTM model scope
* #return bool true
*/
public function afterValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
unset($this->data[$this->name][$model]);
if(isset($this->validationErrors[$model]))
$this->$model->validationErrors[$model] = $this->validationErrors[$model];
}
return true;
}
}
After this, you can use your validation in you model like this :
class Post extends AppModel {
public $validate = array(
// [...]
'Tag' => array(
// here we ask for min 1 tag
'rule' => array('multiple', array('min' => 1)),
'required' => true,
'message' => 'Please select at least one Tag for this Post.'
)
);
/** #var array many Post belong to many Tag */
public $hasAndBelongsToMany = array(
'Tag' => array(
// [...]
)
);
}
This answer use :
Painless HABTM Validation in CakePHP by #jesal
HABTM form validation in CakePHP
CakePHP 2.x Saving and validating a HABTM relation example
Related
Okay, so I have a question. I'm programming a really complex report and the interface uses Laravel 5.2. Now the thing is that, depending on certain conditions, the user does not always need all parameters to be filled. However, for simplicity purposes, I made it so that the report always receives the complete set of parameters no matter what. So I have three tables:
tblReportParam
ID
ParamName
DefaultValue
tblReportParamValue
ParamID
ReportID
Value
tblReport
ID
UserName
Now, I have a solution that works, but for some reason, it just feels like I should be able to make better use of models and relationships. I basically have just my models and controllers and solved the whole thing using SQL.
It feels somewhat close to this but not quite. So basically, you need to always load/save all parameters. If parameter x is actually defined by the user then you use his definition otherwise you go with the default defined in tblReportParam. Anyone has any idea how to do this?
EDIT:
Okay, so I checked Eddy's answer and tried to work it in our system, but another colleague of mine started implementing a many-to-many relationship between the tblReport and the tblReportParam table with the tblReportParamValue acting as the pivot so I'm having some difficulty adapting this solution for our system. Here's the two models:
class ReportParam extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReportParam';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NomParam', 'DefaultValue'];
public function renourapports()
{
return $this->belongsToMany('App\Report');
}
}
class Report extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReport';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NoEmploye', 'NoClient', 'NoPolice', 'DateCreation', 'DateModification', 'runable', 'DernierEditeur'];
public $timestamps = false;
public function params()
{
return $this->belongsToMany('App\ReportParam ', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Valeur');
}
}
Now this actually is a pretty neat solution, but it only works if the parameter is actually in the pivot table (i.e. the relationship actually exists). What we want is that for the parameters that aren't in the pivot table, we simply want their default value. Can Eddy's solution work in this case?
Using Eloquent models
class ReportParam extends Model
{
public function paramValue() {
return $this->hasOne('App\ReportParamValue', 'ParamID');
}
public function getDefaultValueAttribute($value) {
if ( $this->paramValue ) return $this->paramValue->Value; //relationship exists
return $this->DefaultValue;
}
}
$reportParam->value; // return the relationship value or the default value;
UPDATE
Now that tblReportParamValue is a pivot table you should redefine your relationships. In ReportParam model add
public function reports() {
return $this->belongsToMany('App\Report', 'tblReportParamValue', 'ParamID', 'ReportID')->withPivot('Value');
}
And in Report model, defined the opposite
public function params() {
return $this->belongsToMany('App\ReportParam', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Value');
}
Now getting the default value from ReportParam becomes too complicated because it will one ReportParam has Many Reports. So doing $reportParam->reports() will bring back every single report that uses that paramID in the pivot table. Therefore looking for a value would mean going through all the reports. We could avoid that by changind the function definition.
public function getDefaultValue($reportID) {
$reportValue = $this->reports()->wherePivot('ReportID', $reportID)->first();
return $reportValue ? $this->reportValue->Value : $this->DefaultValue;
}
//In Controller
$report = Report::find(1);
$reportParam = ReportParam::find(1);
$reportParam->getDefaultValue($report->ID);
Ok I think this might work. If it doesnt, I am really sorry, I don't know any better.
Is there way to show url with filter to another entity list instead of showing all related entities?
My entity has OneToMany reference to it's events:
/**
*
* #ORM\OneToMany(targetEntity="Event", mappedBy="organizer", cascade={"ALL"})
*/
private $events;
$formMapper->add('events') shows me select2 input with all events.
I just want to show a link to events list with filter to current owner.
I'm using Symfony 2.5.
Yes, this is possible.
You have to get the current owner and create a custom query builder to get only the events related to the owner identifier.
protected function configureFormFields(FormMapper $formMapper)
{
// get current owner
$ownerId = $this->subject->getId();
// using query_builder to create a custom query based on current owner
$formMapper->add('events', null, array(
'query_builder' => function(EntityRepository $er) use ($ownerId) {
$events = $er->createQueryBuilder('e');
if ($ownerId != null) {
$events = $er->where('e.owner = :ownerId')
->setParameter('ownerId', $ownerId);
}
return $events;
}
));
}
Also don't forget to add the use for EntityRepository :
use Doctrine\ORM\EntityRepository;
I have an Orders ActiveRecord with the following relation
/**
* #return array relational rules.
*/
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(
'tests' => array(self::MANY_MANY, 'Test', 'orderstests(orders_id, test_id)'),
);
}
a Test ActiveRecord with the following relations
/**
* #return array relational rules.
*/
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(
'orders' => array(self::MANY_MANY, 'Orders', 'orderstests(test_id, orders_id)'),
);
}
and a many-many relational record
class OrdersTests extends CActiveRecord
I need to get all the tests which are not in a relationship with a particular order, that is a recordset of order_id, test_id doesn't exist for a particular order_id.
I can't seem to find any relations query in Yii for that.
Found the answer on Yii forum
public static function getTestsNotInOrder($order)
{
$excludedTestIds = array_keys($order->tests); // This makes use of the index attribute above
$criteria = new CDbCriteria();
$criteria->addNotInCondition('id', $excludedTestIds);
return self::model()->findAll($criteria);
}
In model:
public function getOptionsGender()
{
array(0=>'Any', 1=>Male', 2=>'Female');
}
In view (edit):
echo $form->dropDownList($model, 'gender', $model->optionsGender);
but I have a CDetailView with "raw" attributes, and it displays numbers instead of genders.
$attributes = array(
...
'gender',
)
What is appropriate way to convert these numbers back to genders? Should I do it in a model, replacing fields such as $this->gender = getOptionsGender($this->gender)? Any github examples will be very appreciated.
I had to choose gender, age, city, country etc. in a few views that are not related to this one. Where should I place my getOptionsGender function definitions?
Thank for your help, the problem is solved.
In model:
public function getGenderOptions() { ... }
public function genderText($key)
{
$options = $this->getGenderOptions();
return $options[$key];
}
In view:
$attributes = array(
array (
'name'=>'gender',
'type'=>'raw',
'value'=>$model->genderText($model->gender), //or $this->genderText(...)
),
);
$this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>$attributes,
));
The working example can be found here:
https://github.com/cdcchen/e23passport/blob/c64f50f9395185001d8dd60285b0798098049720/protected/controllers/UserController.php
In Jeffery Winsett's book "Agile Web Application Development with Yii 1.1", he deals with the issue using class constants in the model you are using. In your case:
class Model extends CActiveRecord
{
const GENDER_ANY=0;
const GENDER_MALE=1;
const GENDER_FEMALE=2;
public function getGenderOptions(){
return array(
self::GENDER_ANY=>'Any',
self::GENDER_MALE=>'Male',
self::GENDER_FEMALE=>'Female',
);
}
public function getGenderText(){
$genderOptions=$this->genderOptions();
return isset($genderOptions[$this->gender]) ? $genderOptions[$this->gender] : "unkown gender({$this->gender})";
}
}
Then in your CDetailView you would have to alter it from gender to:
array(
'name'=>'gender',
'value'=>CHtml::encode($model->genderText()),
),
If several models have the same data, you may want to create a base model that extends CActiveRecord and then extend the new model instead of CActiveRecord. If this model is the only one with that data (ie User model only has gender), but other views use that model to display data, then I would leave it just in the single model class. Also keep in mind that if you place getGenderOptions in the extended class, and you extend ALL your models, they will all have that option available, but may not have the attributes needed and will throw an error if you aren't checking for it.
All this being said, I still think it is a matter or preference. You can handle it however you want, wherever you want. This is just one example from a book I have specifically on Yii.
Im new to cakephp and Im having a little problem querying data.
I have a User model and a Product model in a many to many relation.
What I what is simply for my Products/Index action to only get the products associated to that User (the user is stored in session) and not all Products (which is what it does by default).
Please help.
You only need to set up the relationship properly, the rest is automatic.
Model:
class User extends AppModel {
var $hasAndBelongsToMany = array(
'Product' => array( /* set up relationship */ )
);
}
Controller:
$this->User->recursive = 2; // just to make sure, shouldn't be necessary
$user = $this->User->read(null, $userId);
debug($user);
/**
* $user['User'] contains the user data
* $user['Product'] contains associated products
*/
This should do the trick:
$products = $this->Product->find('all', array(
'conditions' => array(
'User.id' => $user_id_from_session
)
));