Cakephp, validate multiple models manually - php

I have a form that produces the following array upon submition (see below).
I am using this data in my controller to perform several operations, after which I save each one individually. (Saving them all at once is not an option).
What I would need to do is to find a way to validate each of this models.
I have tried already:
$this->Model->set($pertinentData);
$this->Model2->set($pertinentData);
if($this->Model->validates() && $this->Model2->validates()){
//Do whatever
}
This produces unaccurate results, says it validates when I can see it doesn't and viceversa.
Anybody has any idea of a viable option? Ain't there a way to create a tableless model where I can define validation rules for these fields like:
Order.package_id
User.first_name
etc...
Any idea is appreciated. Below the array that the form produces.
Thanks.
Array
(
[Order] => Array
(
[id] => 15
[package_id] => 1743
[tariff_id] => 5470
[tarjeta] => 332
[numero_tarjeta] => 121204045050
[vencimiento_tarjeta] => 10/20
[cod_tarjeta] => 170
[titular_tarjeta] => JESUS CRISTO
[tarjeta_nacimiento] => 22/04/1988
[tarjeta_pais] => Argentina
[tarjeta_provincia] => Buenos Aires
[tarjeta_ciudad] => Ciudad
[tarjeta_cp] => 1428
[tarjeta_calle] => Calle
[tarjeta_numero] => 1477
[tarjeta_piso] => 2
)
[User] => Array
(
[id] =>
[email] => bla8#gmail.com
[phone] => 1568134449
[first_name] => Jesus
[last_name] => Something
[documento_tipo] => dni
[dni] => 335556666
[nacionalidad] => argentino
[birthdate] => 22/04/2019
)
[OrdersCompanion] => Array
(
[1] => Array
(
[first_name] => Chango
[last_name] => Mas
[documento_tipo] => dni
[dni] => 445556666
[nacionalidad] => argentino
[birthdate] => 30/02/2010
)
[1] => Array
(
[first_name] => Chango
[last_name] => Mas
[documento_tipo] => dni
[dni] => 445556666
[nacionalidad] => argentino
[birthdate] => 30/02/2010
)
)
)

You can usea tableless model by defining $useTable= false in the model. Like this
public $useTable = false;
Define all your custom validation and, of course, your schema (since your model has no table you have to define manually the model schema). Then in your controller, you must first indicate that it has no model, and then declare the $model variable. This is to avoid the automatic model-controller binding of cakePHP, your controller would look like this
public $useModel = false;
$model = ClassRegistry::init('ContactOperation');
Now your model is related to your controller as you want, and you can easily make your custom validation, previously defined.
$model->set($this->request->data);
if($model->validates()) {
$this->Session->setFlash(_('Thank you!'));
// do email sending and possibly redirect
// elsewhere for now, scrub the form
// redirect to root '/'.
unset($this->request->data);
$this->redirect('/');
} else {
$this->Session->setFlash(_('Errors occurred.'));
// display the form with errors.
}
You can find more detail from here

Related

CakePHP Issue with saveAll()

I'm having a problem with CakePHP's saveAll() I was hoping someone could shed some light on.
I have a form which collects information to be saved for two models... Person and Inquiry. I believe the data are being sent correctly, but it's simply not saving the Inquiry data. It's returning a validation error, but there is no validation set up in the Inquiry model, and if I remove 'deep' => true from the People controller, it saves those fields fine.
Data It Posts
Array
(
[Person] => Array
(
[first_name] => Test
[middle_name] =>
[last_name] => User
[gender] => M
[date_of_birth] => Array
(
[month] => 02
[day] => 07
[year] => 1994
)
[address] => 1234 Main St
[address_apt] =>
[address_city] => Somewhere
[address_state] => OH
[address_zip] => 304982
[address_country] => US
[phone_cell] => (555) 555-5555
[permission_to_text] => 1
[phone_home] => (555) 555-5556
[email_address] => test#user.com
[preferred_contact] => text_cell
[Inquiry] => Array
(
[admit_type] => FR
[admit_term] => FA2014
[parent_first_name] => Mom
[parent_last_name] => User
[parent_email] => mom#user.com
[hs_name] => Columbus Downtown High School
[hs_ceeb_id] => 365210
[hs_homeschooled] => 0
[hs_grad_year] => Array
(
[year] => 2014
)
[coll_name] =>
[coll_ceeb_id] =>
[coll_major] =>
[coll_year] =>
[admit_major] => 1
[admit_minor] => 4
)
[social_facebook] =>
)
)
Value of $this->Person->validationErrors after Posting
Array
(
[Inquiry] => Array
(
[hs_homeschooled] => Array
(
)
[coll_name] => Array
(
)
[coll_ceeb_id] => Array
(
)
[coll_major] => Array
(
)
[coll_year] => Array
(
)
)
)
Model - Inquiry
<?php
class Inquiry extends AppModel {
public $belongsTo = array('Person');
}
Controller
<?php
class PeopleController extends AppController {
public $helpers = array('Html', 'Form', 'Country', 'State', 'Major', 'Minor', 'Term');
public function index() {
if ($this->request->is('post')) {
$this->Person->create();
if ($this->Person->saveAll($this->request->data, array('deep' => true))) {
print_r($this->request->data);
$this->Session->setFlash(__('Your post has been saved.'));
return $this->redirect(array('action' => 'index'));
}
print_r($errors = $this->Person->validationErrors);
$this->set('errors', $errors = $this->Person->validationErrors);
$this->Session->setFlash(__('Unable to add.'));
}
}
}
Model::saveAll() is a wrapper of saveMany or saveAssociated.
Since you're not passing data in a numerical indexed array, i.e.
array(
0 => array(...),
1 => array(...),
)
it means saveAssociated is called. What you're apparently trying to do is to save a new Person together with the associations (Inquiry). If you read through the manual you'll come to this paragraph:
If neither of the associated model records exists in the system yet
(for example, you want to save a new User and their related Profile
records at the same time), you’ll need to first save the primary, or
parent model.
So you obviously need to save the Person first and then all associations.

cakephp - sorting by a second level association in paginate

I am playing around with a quotes database relating to a ski trip I run. I am trying to list the quotes, but sort by the person who said the quote, and am struggling to get the paginate helper to let me do this.
I have four relevant tables.
quotes, trips, people and attendances. Attendances is essentially a join table for people and trips.
Relationships are as follows;
Attendance belongsTo Person hasMany Attendance
Attendance belongsTo Trip hasMany Attendance
Attendance hasMany Quote belongs to Attendance
In the QuotesController I use containable to retrieve the fields from Quote, along with the associated Attendance, and the fields from the Trip and Person associated with that Attendance.
function index() {
$this->Quote->recursive = 0;
$this->paginate['Quote'] = array(
'contain' => array('Attendance.Person', 'Attendance.Trip'));
$this->set('quotes', $this->paginate());
}
This seems to work fine, and in the view, I can echo out
foreach ($quotes as $quote) {
echo $quote['Attendance']['Person']['first_name'];
}
without any problem.
What I cannot get to work is accessing/using the same variable as a sort field in paginate
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
or
echo $this->Paginator->sort('Location', 'Attendance.Trip.location');
Does not work. It appears to sort by something, but I'm not sure what.
The $quotes array I am passing looks like this;
Array
(
[0] => Array
(
[Quote] => Array
(
[id] => 1
[attendance_id] => 15
[quote_text] => Hello
)
[Attendance] => Array
(
[id] => 15
[person_id] => 2
[trip_id] => 7
[Person] => Array
(
[id] => 2
[first_name] => John
[last_name] => Smith
)
[Trip] => Array
(
[id] => 7
[location] => La Plagne
[year] => 2000
[modified] =>
)
)
)
I would be immensely grateful if someone could suggest how I might be able to sort by the the first_name of the Person associated with the Quote. I suspect my syntax is wrong, but I have not been able to find the answer. Is it not possible to sort by a second level association in this way?
I am pretty much brand new with cakephp so please be gentle.
Thanks very much in advance.
I've had the similar problem awhile back. Not with sort though. Try putting the associated table in another array.
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
change to:
echo $this->Paginator->sort('Name', array('Attendance' => 'Person.first_name'));
Hope this helps
i'm also looking for help with this.
so far i've found that you can sort multi level associations in controller's pagination options after using the linkable plugin https://github.com/Terr/linkable.
but it breaks down when you try to sort form the paginator in the view. i'm using a controller for magazine clippings. each clipping belongs to an issue and each issue belongs to a publication.
$this->paginate = array(
"recursive"=>0,
"link"=>array("Issue"=>array("Publication")),
"order"=>array("Publication.name"=>"ASC",
"limit"=>10);
after debugging $this->Paginator->params->paging->Clipping in the view, you can see that the sort is described in two separate places, "defaults" and "options". the sort info needs to be present in both for it to work in the view.
here is after setting order in controller:
[Clipping] => Array
(
[page] => 1
[current] => 10
[count] => 6685
[prevPage] =>
[nextPage] => 1
[pageCount] => 669
[defaults] => Array
(
[limit] => 10
[step] => 1
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => ASC
)
[conditions] => Array
(
)
)
[options] => Array
(
[page] => 1
[limit] => 10
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => ASC
)
[conditions] => Array
(
)
)
)
and here is after using $this->Paginator->sort("Publication","Publication.name");.
notice the options array is empty.
[Clipping] => Array
(
[page] => 1
[current] => 10
[count] => 6685
[prevPage] =>
[nextPage] => 1
[pageCount] => 669
[defaults] => Array
(
[limit] => 10
[step] => 1
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
[Publication.name] => DESC
)
[conditions] => Array
(
)
)
[options] => Array
(
[page] => 1
[limit] => 10
[recursive] => 0
[link] => Array
(
[Issue] => Array
(
[0] => Publication
)
)
[order] => Array
(
)
[conditions] => Array
(
)
)
does one really need to modify the paginator class to make this work?
UPDATE:
i found out the problem:
in the core cake controller paginator merges default and options to create the find query.
but the options array is empty when using linkable to sort. because options is listed after default it overrides default and the empty array replaces the default array of options.
solution to this is extending the paginate function inside of app_controller.php and unsetting the options array order value if it is empty:
(line 1172 in cake/libs/controller/controller.php)
if(empty($options["order"])){
unset($options["order"]);
}
then the options will not be overwritten by thte blank array.
of course this should not be changed inside of controller.php, but put it in app_controller.php and move it to your app folder.
On CakePHP 3 this problem can be solved by adding 'sortWhitelist' params to $this->paginate on your controller.
$this->paginate = [
// ...
'sortWhitelist' => ['id', 'status', 'Attendance.Person.first_name']
];
And then in your view:
echo $this->Paginator->sort('Name', 'Attendance.Person.first_name');
This is noted in the docs:
This option is required when you want to sort on any associated data, or computed fields that may be part of your pagination query:
However that could be easily missed by tired eyes, so hope this helps someone out there!

Cakephp: question about saveall() with multiselect

I'm wondering what the cleanest way is to implement a cakephp form where 1 control is a multi-select and the rest are text fields or single-selects, and then the data is inserted as multiple rows with a saveall(). So for example a form is selected with these values:
textfield A
value=Foo
mulit-select B
values=US,Mexico,Canada
single=select C
value=10
and so I want to insert these rows into the database with a saveall():
Foo,US,10
Foo,Mexico,10
Foo,Canada,10
Now I know in the add view I can use this format for the input statement:
input('Model.0.field1',...)
but I'm wondering if I can mix that in that same form with inputs formatted like
input('Model.field2',....).
Update:
When I mix and match the single-select and multiple-select controls, the form data gets submitted like this:
Array
(
[Alert] => Array
(
[schedule_id] => 75
[user_id] => 6
[0] => Array
(
[frequency] => Array
(
[0] => WEEKLY
[1] => MONTHLY
)
)
[limit_value] => .03
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 0
)
)
I tried passing that data into saveall() but it treats it like a single record.
Update2: I think saveAll() requires that the multiple rows of data be formatted like this:
Array
(
[Article] => Array(
[0] => Array
(
[title] => title 1
)
[1] => Array
(
[title] => title 2
)
)
)
So it looks like after the submit I'm going to need some javascript code that will restructure the array.
I have something that works... I'm not sure if it takes full advantage of all of cake's "automagic" capabilities, but I don't think it's too convoluted.
So I just added the following code to my controller's add function:
if (!empty($this->data)) {
//debug($this->data, true);
/* begin custom code */
$multiselect = $this->data['Alert']['entity_id'];
$tmp2 = array();
foreach ($multiselect as $item)
{
$tmp = $this->data['Alert'];
$tmp['entity_id'] = $item;
array_push($tmp2,$tmp);
}
$this->data['Alert'] = $tmp2;
debug($this->data,true);
/* end custom code */
$this->Alert->create();
//restructure data
if ($this->Alert->saveAll($this->data['Alert'])) {
$this->Session->setFlash(__('The alert has been saved', true));
//$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The alert could not be saved. Please, try again.', true));
}
and that converts my data to this:
Array
(
[Alert] => Array
(
[0] => Array
(
[schedule_id] => 74
[entity_id] => 1
[user_id] => 6
[frequency] => HOURLY
[limit_value] => .02
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 1
)
[1] => Array
(
[schedule_id] => 74
[entity_id] => 2
[user_id] => 6
[frequency] => HOURLY
[limit_value] => .02
[limit_adjustment] => 0
[type] => LIMIT
[disabled] => 1
)
)
)

Modifying Drupal Form Fields - [#weight] in array not being respected?

I don't have experience in php. I've followed a few tutorials to modify my Drupal forms using the theme method in template.php.
For some reason the [#weight] property for a field does not adhere to its values. I'd like to move the Category field [cid] above Subject field [subject].
These are the lines of code I used:
$form['cid']['#weight'] = 0.003;
$form['subject']['#weight'] = 0.004;
When I print my array to view I see the values have changed, but when I render the form no changes are made. I have already cleared performance cache after every modification.
Here is a snippet of my printed array:
[subject] => Array
(
[#type] => textfield
[#title] => Subject
[#maxlength] => 255
[#required] => 1
[#post] => Array
(
)
[#programmed] =>
[#tree] =>
[#parents] => Array
(
[0] => subject
)
[#array_parents] => Array
(
[0] => subject
)
[#weight] => 0.004
[#processed] => 1
[#description] =>
[#attributes] => Array
(
)
[#input] => 1
[#size] => 60
[#autocomplete_path] =>
[#process] => Array
(
[0] => form_expand_ahah
)
[#name] => subject
[#id] => edit-subject
[#value] =>
[#defaults_loaded] => 1
[#sorted] => 1
)
[cid] => Array
(
[#type] => select
[#title] => Category
[#default_value] => 1
[#options] => Array
(
[1] => General Enquiries
[2] => Support
)
[#required] => 1
[#post] => Array
(
)
[#programmed] =>
[#tree] =>
[#parents] => Array
(
[0] => cid
)
[#array_parents] => Array
(
[0] => cid
)
[#weight] => 0.003
[#processed] => 1
[#description] =>
[#attributes] => Array
(
)
[#input] => 1
[#size] => 0
[#multiple] =>
[#process] => Array
(
[0] => form_expand_ahah
)
[#name] => cid
[#id] => edit-cid
[#value] => 1
[#defaults_loaded] => 1
[#sorted] => 1
)
You generally see that behavior on node forms when CCK is enabled. This is because CCK overrides weight for the default and CCK fields with its own ordering system configurable by going to Content management -> Content types -> [Content Type] -> Manage fields.
Of course, if you disable CCK (probably not an option), your #weight values will be respected.
One thing to note is that while Forms API allows decimals for #weight, it's good practice to use integers.
Edit
If you want to work within the constraints CCK presents, in a custom module you'll need to implement a #pre_render handler that will alter the weights of the form elements after CCK alters them:
function mymodule_form_alter(&$form, &$form_state, $form_id) {
$form['#pre_render'][] = 'mymodule_form_alter_weight';
}
function mymodule_form_alter_weight($elements) {
$elements['cid']['#weight'] = 0.003;
$elements['subject']['#weight'] = 0.004;
return $elements;
}
The problem is, you will have no idea what weight values CCK has used for other form elements, so while you'll have ordered cid before subject, both fields might show up in the middle of all the other fields on the page and not in their original position.
CCK forces you into a corner when it comes to weight. The correct, Drupal/CCK way to handle form ordering is to respect the choices made by an administrator on Content management -> Content types -> [Content type] -> Manage fields.
If you have a custom form element, you can tell CCK about it so it shows up in Manage fields by implementing hook_content_extra_fields(). Continuing with the code above:
function mymodule_content_extra_fields() {
$extras['mymodule'] = array( // Name of field
'label' => t('Mymodule'),
'description' => t('Mymodule field'),
'weight' => 10, // The default weight, can be overriden on Manage Fields
);
return $extras;
}

Modifying Model data in beforeSave of Behavior when using saveAll

I'm trying to write a Meta behavior for a project I am working on that will allow me to assign custom variables/attributes to a model, similar to how you can create custom fields in a wordpress post.
I have created a Meta behavior that binds the meta model to the model it is acting on and also has a beforeSave callback that loops through the models data variable and puts the model name into the meta array.
Everything is saving but when I check the database the model field is coming back empty.
The database structure for the Meta is
id - A unique if for the meta
model - The name of the model that this meta entry is associated with
foreign_id - The id of the above model that this meta entry is associated with
key - Name of the meta variable
value - Value of the meta variable
The data that comes to the saveAll function from the form is
Array
(
[Page] => Array
(
[id] => 12
[name] => Test
[slug] => a/b/c/d
[layout] => 0
[body] => Test multilevel
)
[Meta] => Array
(
[0] => Array
(
[id] => 1
[key] => page_title
[value] => About Us
)
[1] => Array
(
[id] => 6
[key] => test4
[value] => test
)
[2] => Array
(
[key] => test3
[value] => lala
)
)
)
and after it has run through the behavior beforeSave it is
Array
(
[Page] => Array
(
[id] => 12
[name] => Test
[slug] => a/b/c/d
[layout] => 0
[body] => Test multilevel
[modified] => 2010-05-04 15:56:54
)
[Meta] => Array
(
[0] => Array
(
[id] => 1
[key] => page_title
[value] => About Us
[model] => Page
)
[1] => Array
(
[id] => 6
[key] => test4
[value] => test
[model] => Page
)
[2] => Array
(
[key] => test3
[value] => lala
[model] => Page
)
)
)
The code from the behavior is
<?php
/**
* Meta Model Behavior
*
* Adds custom variables to models
*
**/
class MetaBehavior extends ModelBehavior {
/**
* Contains configuration settings for use with individual model objects.
* Individual model settings should be stored as an associative array,
* keyed off of the model name.
*
* #var array
* #access public
* #see Model::$alias
*/
var $__settings = array();
var $__defaults = array(
'class' => 'Meta',
'foreign_key' => 'foreign_id',
'dependent' => true,
'auto_bind' => true
);
/**
* Initiate Meta Behavior
*
* #param object $model
* #param array $config
* #return void
* #access public
*/
function setup(&$model, $settings = array()) {
$default = $this->__defaults;
$default['conditions'] = array('Meta.model' => $model->alias);
if (!isset($this->__settings[$model->alias])) {
$this->__settings[$model->alias] = $default;
}
$this->__settings[$model->alias] = array_merge($this->__settings[$model->alias], ife(is_array($settings), $settings, array()));
if ($this->__settings[$model->alias]['auto_bind']) {
$hasManyMeta = array(
'Meta' => array(
'className' => $this->__settings[$model->alias]['class'],
'foreignKey' => $this->__settings[$model->alias]['foreign_key'],
'dependent' => $this->__settings[$model->alias]['dependent'],
'conditions' => $this->__settings[$model->alias]['conditions']
)
);
$metaBelongsTo = array(
$model->alias => array(
'className' => $model->alias,
'foreignKey' => $this->__settings[$model->alias]['foreign_key']
)
);
$model->bindModel(array('hasMany' => $hasManyMeta), false);
$model->Meta->bindModel(array('belongsTo' => $metaBelongsTo), false);
}
}
function beforeSave(&$model) {
foreach($model->data[$this->__settings['class']] as $key => $value) {
$model->data[$this->__settings['class']][$key]['model'] = $model->alias;
}
return true;
}
} // End of MetaBehavior
?>
I have a feeling it may be because of the relationships and the saveall is using the passed data (original) when saving the associations.
The only other way I thought of doing it would be to remove the relationships and put some code into the afterSave function of the behavior to handle the saving and then put some other code into the afterFind to retrieve them.
Any ideas?
Cheers,
Dean
I don't believe you'll have any luck with this as Model::saveAll() doesn't call beforeSave() at all. In fact, it loads associations before then calling $this->save().
Line 1652 of the Model source shows the associations being loaded prior to any real call to Model::__save() that doesn't just validate the data.
What this means is, at first glance, it looks like the magic bindModel() call in your MetaBehavior won't have any affect. To be honest, I'd not really bother with something like that as it's just simpler to set the association in your Page Model class definition.
Of course, you could:
Create an afterSave in your MetaBehavior that facilitates the saving of the Meta Model data or,
Override saveAll() in the model to add the binding and then call parent::saveAll()
Ultimately, I feel it's more sane to utilise Model::saveAll() and set a real association in your Model class definition anyway. Utilising behaviours to promote DRY practices is very noble but it doesn't mean you need to make things less trivial as part of a development methodology that you or another person follows
TLDR; You've already added Meta to your behaviors array, adding an association is only another few lines of code within the same file.

Categories