I have a field 'screenshot' that when I try to access in beforeSave callback the field it's empty.
The thing I do is access the $data on the beforeMarshal callback and store the array
into a model setting, then I can access that in the beforeSave and set 'screenshot' field to filename.ext if move_uploaded_file is true.
This is the current code:
Model
// Using CakePHP 3.8.5
public function validationDefault(Validator $validator)
{
$validator
->allowEmptyFile('screenshot', 'update')
->uploadedFile('screenshot' , [
'types' => ['image/jpeg', 'image/jpg', 'image/pjpeg'],
'maxSize' => 1000000 // 1MB
]);
return $validator;
}
public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
{
if (isset($data['screenshot']) && $data['screenshot']['error'] === UPLOAD_ERR_OK) {
$this->config([ 'file_array' => $data['screenshot'] ])
}
}
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
{
...
$file = $this->config([ 'file_array');
if (move_uploaded_file($file['tmp_name'], $file_path)) {
return true;
} else {
throw new Exception(__('Unable to move...'));
}
}
Form
<?= $this->Form->create($project, ['type' => 'file']) ?>
<?= $this->Form->control('screenshot', ['type' => 'file', 'class' => 'form-control-file']) ?>
<?= $this->Form->button(__('Submit'), ['class' => 'btn btn-primary col-md-3 offset-md-9']) ?>
<?= $this->Form->end() ?>
The code I expected to work
public function validationDefault(Validator $validator)
{
$validator
->allowEmptyFile('screenshot', 'update')
->uploadedFile('screenshot' , [
'types' => ['image/jpeg', 'image/jpg', 'image/pjpeg'],
'maxSize' => 1000000 // 1MB
]);
return $validator;
}
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
{
...
$file = $entity->screenshot; // this is empty
if (move_uploaded_file($file['tmp_name'], $file_path)) {
return true;
} else {
throw new Exception(__('Unable to move...'));
}
}
Why is $entity->screenshot empty on BeforeSave?
Is this the correct way to do this?
At marshalling time (when patching/creating entities), CakePHP will cast/convert the input according to the database types mapped for the fields.
For your database field screenshot, which is VARCHAR, that would be \Cake\Database\Type\StringType, which returns an empty string for arrays. The reasoning being that the marshalling stage shouldn't cause "crashes", but ideally create entities with data compatible to the respective database types, which can finally be validated via application rules if necessary, for which the errors can be as easily presented to the user as the ones for the validation rules.
The two most popular ways to handle this IMHO are:
Using a different field name for the upload, one that isn't mapped to an existing database column, eg something like screenshot_upload
Using a custom database type for the field, one that doesn't transform the array
Personally I prefer the former.
Related
Previously, I asked the following question.
When I'am in the edit-screen and I set the value "Done" in the field "Project_status", is it then possible that I can force me to change the field "Afgehandeld" to the value "Arjan" automaticly.
Because I was not able to add my code to the provided answeres, I asked it again, but now with the code that I already have.
This code is unfortunily not working
Who can further help this newbie?
Thanks in advance.
<?php
/* src/template/projecten/index.ctp */
foreach ($projecten as $DB_inhoud):
...
...
echo $DB_inhoud->Project_status;
echo $DB_inhoud->Opgeleverd;
...
...
endforeach;
?>
<?php
/* src/template/projecten/edit.ctp */
...
echo $this->Form->select('Project_status', $status, ['escape' => true]);
echo $this->Form->input('Opgeleverd',['label' => false]);
...
...
?>
<?php
/* src/Model/Table/ProjectenTable.php */
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\RulesChecker;
Class ProjectenTable extends Table
{
Public function initialize (array $config)
{
$this->addBehavior('Timestamp');
}
public function customValidationMethod($check, array $context)
{
$validator->add('Projecten->Opgeleverd', 'custom',
['rule' => function ($value, $context)
{
echo $context;
if ($context['Projecten->Project_status'] == "100. Project opgeleverd")
{
return $value == "Arjan";
}
return true;
},
'message' => 'Error message'
]);
}
}
I have a problem with my cakephp3 app. I try to import some subscribers from a CSV file.
I use a Behavior to import my file (https://github.com/ProLoser/CakePHP-CSV). I haven't manage to load the plugin, so I have put the Behavior file on my project under src/Model/Behavior.
I have added the behavior to my SubscribersTable.
I have created a new view (import/export) and I have added this method to my SubscribersController :
public function importExport()
{
if ($this->request->is('post'))
{
$data = $this->request->data;
if(!empty($data['import_file']['name']))
{
$file = $data['import_file'];
if ((isset($file['tmp_name']) &&($file['error'] == UPLOAD_ERR_OK)))
{
$subscribersData = $this->Subscribers->importCsv($file['tmp_name']);
$entities = $this->Subscribers->newEntities($subscribersData);
$table = $this->Subscribers;
$table->connection()->transactional(function () use ($table, $entities) {
foreach ($entities as $entity) {
$table->save($entity, ['atomic' => false]);
}
});
}
}
}
}
The data I get seems to be good, as the entities created, but, when I call $table-save(), I have this error :
Table "App\Model\Table\SubscribersTable" is not associated with "request"
I have read some other questions on stackoverflow, but I don't understand why I have this error. I am new to cakephp so I don't understand everything...
If can someone help me...
Thanks !!
EDIT : It seems during the debug, the behavior is strange. Maybe this error has nothing to see with my real problem.
Here is the debug timeline :
$table->save() call:
if ($options['atomic']) {
$success = $connection->transactional(function () use ($entity, $options) {
return $this->_processSave($entity, $options);
});
} else {
===> $success = $this->_processSave($entity, $options);
}
...
$this->_processSave call :
$data = $entity->extract($this->schema()->columns(), true);
and during the extract, no column is retrieved because
public function extract(array $properties, $onlyDirty = false)
{
$result = [];
foreach ($properties as $property) {
if (!$onlyDirty || $this->dirty($property)) {
$result[$property] = $this->get($property);
}
}
return $result;
}
$onlyDirty = true and $this->dirty($property)) return false.
So, when this function is called
$success = $this->_insert($entity, $data);
as the data is null, nothing is saved.
I don't really understant the concept of dirty. In the doc, it seems it is usefull when working with BelongToMany, but this element has no link with other tables, so if someone can clarify this concept ?
SubscribersTable :
<?php
namespace App\Model\Table;
use App\Model\Entity\Subscriber;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Subscribers Model
*
*/
class SubscribersTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('subscribers');
$this->displayField('email');
$this->primaryKey('email');
$options = array(
// Refer to php.net fgetcsv for more information
'length' => 0,
'delimiter' => ',',
'enclosure' => '"',
'escape' => '\\',
// Generates a Model.field headings row from the csv file
'headers' => true,
// If true, String $content is the data, not a path to the file
'text' => false,
);
$this->addBehavior('Csv', $options);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->add('email', 'valid', ['rule' => 'email'])
->allowEmpty('email', 'create');
$validator
->allowEmpty('contact');
$validator
->allowEmpty('company');
$validator
->allowEmpty('postal_code');
$validator
->add('vip', 'valid', ['rule' => 'boolean'])
->allowEmpty('vip');
$validator
->add('indoor', 'valid', ['rule' => 'boolean'])
->allowEmpty('indoor');
$validator
->add('live', 'valid', ['rule' => 'boolean'])
->allowEmpty('live');
$validator
->add('prod', 'valid', ['rule' => 'boolean'])
->allowEmpty('prod');
$validator
->allowEmpty('localisation');
$validator
->allowEmpty('family');
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->isUnique(['email']));
return $rules;
}
}
This error likely comes from inside your SubscribersTable.php.
If you have $this->request ... in that file, it will throw that error because it's attempting to find request as an associated model, since it's not a valid method to call from a model.
Or, if it's in your controller, you likely have $this->Subscribers->request...
It could also be from inside another Model if you refer to it like this: $this->Subscribers->request ...
Ok.
As I thought, the problem wasn't this error (probably due to the use of the debugger), but it was the structure of my input file.
The input file must be formatted as indicated in this document :
http://book.cakephp.org/3.0/en/orm/saving-data.html#converting-multiple-records
The problem has been resolved !
Thanks again for you help !
I'm creating a change password form in CakePHP, which requires that a user enter his/her current password, new password, and confirm new password. After all that is entered, the user's password will be changed, given that all entries follow the validation rules that are set.
However, the validation rules aren't working. The user doesn't have to fill in all of the fields, set the new password to a certain length, make sure that the new password and confirm password fields match, etc. I cant seem to find out what the problem is.
I'll provide all of the relevant code below. Thanks :)
change_password.ctp (The view file)
<div class="users form large-9 medium-9 columns">
<?= $this->form->create() ?>
<fieldset>
<legend> <?= __('Change Password') ?> </legend>
<?= $this->Form->input('current_password', ['type' => 'password']) ?>
<?= $this->Form->input('new_password', ['type' => 'password']) ?>
<?= $this->Form->input('confirm_new_password', ['type' => 'password']) ?>
</fieldset>
<?= $this->Form->button(__('Save')) ?>
<?= $this->Form->end() ?>
</div>
changePassword() in UsersController
public function changePassword()
{
$user = $this->Users->get($this->Auth->user('id'));
if (!empty($this->request->data)) {
$user = $this->Users->patchEntity($user, [
'password' => $this->request->data['new_password'],
],
['validate' => 'password']
);
if ($this->Users->save($user)) {
$this->Flash->success('The password is successfully changed');
$this->redirect('/users');
} else {
$this->Flash->error('There was an error during the save!');
}
}
$this->set('user', $user);
}
validationPassword() in UsersTable (i.e The validation rules)
public function validationPassword(Validator $validator )
{
$validator
->add('current_password','custom',[
'rule'=> function($value, $context){
$user = $this->get($context['data']['id']);
if ($user) {
if ((new DefaultPasswordHasher)->check($value, $user->password)) {
return true;
}
}
return false;
},
'message'=>'Incorrect Password!',
])
->notEmpty('current_password');
$validator
->add('new_password', [
'length' => [
'rule' => ['minLength', 6],
'message' => 'The password must be at least 6 characters!',
]
])
->add('new_password',[
'match'=>[
'rule'=> ['compareWith','confirm_new_password'],
'message'=>'The passwords does not match!',
]
])
->notEmpty('new_password');
$validator
->add('confirm_new_password', [
'length' => [
'rule' => ['minLength', 6],
'message' => 'The password must be at least 6 characters!',
]
])
->add('confirm_new_password',[
'match'=>[
'rule'=> ['compareWith','new_password'],
'message'=>'The passwords does not match!',
]
])
->notEmpty('confirm_new_password');
return $validator;
}
EDIT: Added User.php file
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;
use Cake\Validation\Validator;
/**
* User Entity.
*
* #property int $id
* #property string $first_name
* #property string $last_name
* #property string $email
* #property string $username
* #property string $password
*/
class User extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* #var array
*/
protected $_accessible = [
'*' => true,
'id' => false,
];
/**
* Fields that are excluded from JSON an array versions of the entity.
*
* #var array
*/
protected $_hidden = [
'password'
];
protected function _setPassword($value) {
$hasher = new DefaultPasswordHasher();
return $hasher->hash($value);
}
}
You have validation rules for fields current_password, new_password and confirm_new_password but you are only setting the field password with patchEntity that's why this validators are not called.
public function changePassword()
{
...
$user = $this->Users->patchEntity($user, [
'password' => $this->request->data['new_password'], // There is no validator defined for the field 'password'
],
['validate' => 'password']
);
...
}
In your case you must change your code with
public function changePassword()
{
...
$user = $this->Users->patchEntity($user, [
'password' => $this->request->data['new_password'], // Your actual field in db
'current_password' => $this->request->data['current_password'], // Trigger validation rule current_password
'new_password' => $this->request->data['new_password'], // Trigger validation rule new_password
'confirm_new_password' => $this->request->data['new_password'], // Trigger validation rule confirm_new_password
],
['validate' => 'password']
);
...
}
In the view file you have also forgot to pass the entity as first parameter of Form::create()
I have a form that collects data about an Article, and I want to save that data, as well as for a model called Abstract, where an Article hasMany Abstracts. My models look like this:
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class AbstractsTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Articles');
}
public function validationDefault(Validator $validator)
{
$validator
->notEmpty('body');
return $validator;
}
}
And
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class ArticlesTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$this->hasMany('Abstracts');
}
public function validationDefault(Validator $validator)
{
$validator ->notEmpty('category')
return $validator;
}
}
My input form has a field named 'abstracts.body', and in my ArticlesController I have this function:
public function add()
{
$data = $this->request->data;
$article = $this->Articles->newEntity($data, [
'associated' => ['Abstracts']
]);
if ($this->request->is('post')) {
$article->user_id = $this->Auth->user('id');
$data['abstracts']['user_id'] = $article->user_id;
$data['abstracts']['approved'] = 0;
$article = $this->Articles->patchEntity($article, $data, [
'associated' => ['Abstracts']
]);
if ($this->Articles->save($article, [ 'validate' => false,
'associated' => ['Abstracts']
]) )
{
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$this->set('article', $article);
}
My Abstracts table is pretty straightforward:
CREATE TABLE 'abstracts' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'article_id' INTEGER , 'user_id' INTEGER , 'body' TEXT, 'approved' BOOLEAN )
From debugging I can see that I have the correct 'abstracts' array within my $data (in add()), but it doesn't appear to ever try to save it to the database. Can someone please point out my error? Thanks!
Got it.
I started going wrong here:
My input form has a field named 'abstracts.body'
Because it's a hasMany relationship, I need to have that input be 'abstracts.0.body'
Then the rest of LeWestopher's answer will work-- adding an index to the fields I want to fill in from the Controller, so $data[abstracts][0]['user_id'] => ... and so on. Thanks!
You're post processing your $data['abstracts'] array incorrectly resulting in the association not saving. $data['abstracts'] is expected to be an array of Abstracts. Your issue lies here:
$data['abstracts']['user_id'] = $article->user_id;
$data['abstracts']['approved'] = 0;
You should be able to fix this pretty easily by changing this to:
foreach($data['abstracts'] as $index => $abstract) {
$abstract['user_id'] = $article->user_id;
$abstract['approved'] = 0;
$data['abstracts'][$index] = $abstract;
}
This should correctly iterate over your array of abstracts, set the user_id and approved keys appropriately and then it should save correctly.
CakePHP 3.x Documentation on Saving Associations
EDIT: Very interesting issue indeed. Try it without using patchEntity, and use newEntity by itself instead:
public function add()
{
if ($this->request->is('post')) {
$data = $this->request->data;
// Post process abstracts objects
foreach($data['abstracts'] as $index => $abstract) {
$abstract['user_id'] = $article->user_id;
$abstract['approved'] = 0;
$data['abstracts'][$index] = $abstract;
}
// Build newEntity
$article = $this->Articles->newEntity($data, [
'associated' => ['Abstracts']
]);
// Save our entity with associations
if ($this->Articles->save($article, [
'validate' => false,
'associated' => ['Abstracts']
])) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
// On save fail
$this->Flash->error(__('Unable to add your article.'));
$this->set('article', $article);
}
}
EDIT 2: Your issue looks like it's definitely in your form helper. Your current form helper input creates an $data array that looks like this:
$data = [
'abstracts' => [
'body' => 'example text'
],
'category' => 'Science'
];
Which SHOULD look like:
$data = [
'abstracts' => [
['body' => 'example text'],
['body' => 'Im your second abstract'],
['body' => 'Abstract three!']
],
'category' => 'Science'
];
The issue lies in:
abstracts.body
Which should read as (in array dot notation):
// abstracts.0.body
echo $this->Form->input('abstracts.0.body', [
'label' => 'summary of article',
'maxlength' =>'440',
'rows' => '7'
]);
I believe that should be the last issue you run into.
I'm building a form which contains a category field. I need a choice list to do that, but I don't find out how to fill this choice list with the several categories stored in the database.
public function buildForm(FormBuilder $builder, array $options) {
$builder->add('item', 'text', array('label' => 'Item'));
$builder->add('category', 'choice', array(
'choices' => ???,
'label' => 'Category'
));
}
How can I get the categories from the database?
(I can't seem to access $this->getDoctrine() inside this class.)
Use type entity instead of choice
$builder
->add('entity_property', 'entity', array(
'class' => 'Namespace\\To\\Entity',
'query_builder' => function(EntityRepository $repository) {
return $repository->createQueryBuilder('q')
->where('q.a_field = yourvalue');
}
));
Edit:
Two ways for using custom parameters in your query. In both situations, the parameters are injected from outside, so your FormType don't need any references to the session or request objects or whatever.
1- Pass required parameters to your constructor
class TaskType extends AbstractType
{
private $custom_value;
public function __construct($custom_value) {
$this->custom_value = $custom_value;
}
// ...
}
in your buildForm() you must copy the value to local variable and make it available for the query_builder callback:
public function buildForm(/*...*/) {
$my_custom_value = $this->custom_value;
// ...
'query_builder' => function(EntityRepository $repository) use ($my_custom_value) {
return $repository->createQueryBuilder('q')
->where('q.a_field = :my_custom_value')
->setParameter('my_custom_value', $my_custom_value);
}
// ...
}
2- use the $options parameter of the buildForm method.
First you have to define a default value by overriding getDefaultOptions:
public function getDefaultOptions(array $options)
{
return array(
'my_custom_value' => 'defaultvalue'
);
}
Then you can pass it from your controller in the third argument of the createForm method.
$this->createForm(new YourFormType(), $entity, array('my_custom_value' => 'custom_value'));
Now the value should be available through the $options parameter of youru buildForm method. Pass it to the callback as described above.
In Symfony 2.1
You now have to use the OptionsResolverInterface within the setDefaultOptions method. Here is the code you would have to use if you wanted to retrieve the options (using the same example as the accepted answer)
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
public function buildForm(FormBuilderInterface $builder, array $options){
parent::buildForm($builder, $options);
$my_custom_value = $options[custom_value];
// ...
'query_builder' => function(EntityRepository $repository) use ($my_custom_value) {
return $repository->createQueryBuilder('q')
->where('q.a_field = :my_custom_value')
->setParameter('my_custom_value', $my_custom_value);
}
// ...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'my_custom_value' => 'defaultvalue'
));
}
You still pass the options in the same way:
$this->createForm(new YourFormType(), $entity, array('my_custom_value' => 'custom_value'));