Pass nested array to Symfony Form for EAV type table - arrays

Take this request data:
HTTP PUT
[
'id' => 1,
'company' => 'Company Name',
'attributes' => [
[
'attribute' => 'url',
'value' => 'example1.com'
],
[
'attribute' => 'url',
'value' => 'example2.com'
],
]
]
I want to build a form that validates attributes using choice loaders:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
$builder->add(
$builder->create('attributes', FormType::class, [
'compound' => true
])
);
$builder->get('attributes')
->add('attribute', ChoiceType::class, [
'constraints' => [
new NotBlank([
'message' => 'attribute name cannot be blank.'
])
],
'choices' => [
'url',
'type',
'email',
'date',
],
])
->add('value', TextType::class)
;
}
However when i write my unit test for this:
public function testAttributesForm(array $data)
{
$form = $this->factory->create(AttributesType::class);
$form->submit($data);
if(!$form->isValid()) {
var_dump($form->getErrors()->current()->getMessage());
}
}
I get a failed validation:
string(42) "This form should not contain extra fields."
Which is because it finds a nested array instead of the actual fields.
How do I pass nested data in a way where I can fully use validation constraints?
In the past I used CollectionType::class with 'allow_add' => true but it doesn't allow for very detailed validation.

You should try use directive
'allow_extra_fields' => true

To be able to validate nested data without a data_class using Forms (as you may when writing an api) you need to add a form per collection dynamically, in this case:
The first form builder method would look like this:
class CompanyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('id', IntegerType::class)
->add('company', TextType::class)
;
$builder->add(
$builder->create('companyAttributes', CollectionType::class, [
'allow_extra_fields' => true
])
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}
and the second would also be a standalone form:
class CompanyAttributeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('value',TextType::class)
->add('attribute',TextType::class, [
'constraints' => [
new NotBlank([
'message' => 'Parameter name cannot be blank.'
])
],
])
;
}
}
When the request is sent, you would create the first and add the second form in a loop.
Notice the loop's $key is used for the form name so you can do $form->get(1) etc.
$form = $this->factory->create(CompanyType::class);
if(!empty($data['attributes'])) {
foreach ($data['attributes'] as $key => $parameters) {
$form->get('companyAttributes')->add($key, CompanyAttributeType::class);
}
}
$form->submit($data);
if($form->isSubmitted() && $form->isValid()) {
//2xx response
} else {
//4xx/5xx response
}
This setup will validate all forms when $form->submit($data) is called.

Related

How can I create a custom list find methd in cakephp-3?

In my code I am reusing a list-find operation with custom keyField and valueField like this:
->find('list', [
'keyField' => 'key',
'valueField' => 'value'
])
I would like to define a custom list-find method that sets the keyField and valueField, so I don't have to repeat myself. Also it should be chainable with additional custom-find methods.
Edit: Simple and clear solution by calling findList inside the custom finder:
public function findCustomList(Query $query, array $options)
{
return $this->findList($query, [
'keyField' => 'key',
'valueField' => 'value',
]);
}
First Answer:
It is possible to use the formatResults-method of the query-Object to register a formatter that will format the results into the desired list:
public function findCustomList(Query $query, array $options)
{
$query->formatResults(function ($results) {
/** #var \Cake\Collection\CollectionInterface $results */
$formatedResults = [];
foreach ($results as $key => $result) {
$formatedResults[$result->key] = $result->value;
}
return new Collection($formatedResults);
});
return $query;
}

Laravel validation for arrays

I have this request:
GET http://example.com/test?q[]=1&q[]=2&q[]=3
And I have this route:
Route::get('test', function(Request $req) {
$req->validate(['q' => 'array']);
});
How should I do to add other validation rules to each element of this array using Laravel validator? For example, I want to check that each q value has a minimum of 2.
Thank you for your help.
Take a look at the documentation about validating arrays.
$validator = Validator::make($request->all(), [
'person.*.email' => 'email|unique:users',
'person.*.first_name' => 'required_with:person.*.last_name',
]);
You can also do this in your controller using the Request object, documentation about validation logic.
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|unique:posts|max:255',
'body' => 'required',
]);
// The blog post is valid...
}
There is a third option for when you have a lot of validation rules and want to separate the logic in your application. Take a look at Form Requests
1) Create a Form Request Class
php artisan make:request StoreBlogPost
2) Add Rules to the Class, created at the app/Http/Requestsdirectory.
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
3) Retrieve the request in your controller, it's already validated.
public function store(StoreBlogPost $request)
{
// The incoming request is valid...
// Retrieve the validated input data...
$validated = $request->validated();
}
You can do:
Route::get('test', function(Request $req) {
$req->validate([
'q' => 'array',
'q.*' => 'min:2'
]);
});
For more information on validation of arrays, see => laravel.com/docs/5.6/validation#validating-arrays
Suppose I got an array of users
users: [
{
"id": 1,
"name": "Jack",
},
{
"id": 2,
"name": "Jon"
}
]
I would validate it like below :
$request->validate([
'users[*]'=> [
"id" => ["integer", "required"],
"name" => ["string", "required"]
]
]);
Here * acts as a placeholder

CakePHP 3 : Unknown method

I am creating a function in model to find all related services.
function in ServiceCategory.php
class ServiceCategory extends Entity
{
public function relatedServices($id)
{
return $this->find('all', [
'conditions' => [
'where' => [
'id !=' => $id
],
'limit' => 5
]
]);
}
}
And calling in ServiceCategoriesController.php
public function view($id = null)
{
$serviceCategory = $this->ServiceCategories->get($id, [
'contain' => ['Services']
]);
$relatedServices = $this->ServiceCategories->relatedServices($id);
$this->set('serviceCategory', $serviceCategory);
$this->set('relatedServices', $relatedServices);
$this->set('_serialize', ['serviceCategory']);
}
But it gives Unknown method 'relatedServices'
Is there any thing wrong I am doing ?
The code is in the wrong class
In the question:
class ServiceCategory extends Entity
This is an entity class
$relatedServices = $this->ServiceCategories->relatedServices($id);
This is making a call on a table object, table objects and entities do not inherit from each other, the method is unavailable to the table class.
Move the code to the table class
The direct solution is to move the code to the table class:
// src/Model/Table/ServiceCategoriesTable.php
namespace App\Model\Table;
class ServiceCategoriesTable extends Table
{
public function relatedServices($id)
{
return $this->find('all', [
'conditions' => [
'where' => [
'id !=' => $id
],
'limit' => 5
]
]);
}
Though the arguably correct/better way to do that is to implement a finder:
// src/Model/Table/ServiceCategoriesTable.php
namespace App\Model\Table;
use Cake\ORM\Query;
use \InvalidArgumentException;
class ServiceCategoriesTable extends Table
{
public function findRelatedServices(Query $query, array $options)
{
if (!isset($options['id'])) {
$message = sprintf('No id in options: %s', json_encode($options));
throw new InvalidArgumentException($message);
}
$query->where(['id !=' => $options['id']);
return $query;
}
Which would be called in exactly the same way as other find calls:
$relatedServices = $this->ServiceCategories->find(
'relatedServices',
['id' => $id]
);

Why a virtual property getter should not be called in cakePHP 3?

in a ctp, I display several virtual properties from 2 entities, computed by getters.
It's ok for one of the both entities but for the other, none of the properties getters are never called.
Here is an excerpt of the ctp:
<table>
....
<td><?= $agTheme->agthemelanguages_wc_string ?></td>
<td><?= $agPoi->paragraph_length_string ?></td>
<td><?= $agPoi->images_string ?></td>
<td><?= $agPoi->audios_string ?></td>
....
</table>
It's ok for AgTheme's property but no getter of the Agpoi's properties is called.
Agtheme model is:
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Collection\Collection;
use Cake\Core\Configure;
use Cake\Log\Log;
use Cake\ORM\TableRegistry;
class Agtheme extends Entity {
protected $_accessible = [
'site_id' => true,
'site' => true,
'name' => true,
'directory_name' => true,
'comment' => true,
'image_0' => true,
'imgpathname0' => true,
'imgfile0' => true,
'mobile_theme' => true,
'agthemelanguages' => true,
'poi_by_tag' => true,
'poi_to_map' => true,
'unlock_mode' => true,
'unlock_code' => true,
'published' => true,
'approved' => true,
];
protected function _getAgthemelanguagesWcString() {
if (isset($this->_properties['agthemelanguages_wc_string'])) {
return $this->_properties['agthemelanguages_wc_string'];
}
if (empty($this->agthemelanguages)) {
return '';
}
$agthemelanguages = new Collection($this->agthemelanguages);
$str = $agthemelanguages->reduce(function ($string, $agthemelanguage) {
return $string . $agthemelanguage->language->name_fr . ' :<br/>';
}, '');
return trim($str, '<br/>');
}
function _getImgpathname0() {
if (isset($this->_properties['imgpathname0'])) {
return $this->_properties['imgpathname0'];
}
if (!isset($this->_properties['id']) || empty($this->image_0) || !isset($this->_properties['directory_name'])) {
return '';
}
$img_path = SITES_CONTENT_URL . $this->_properties['directory_name'] .'/imgTheme_0.' . $this->_properties['image_0'];
return $img_path;
}
}
And Agpoi model is (Agpoi.php):
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Collection\Collection;
class Agpoi extends Entity
{
protected $_accessible = [
'name' => true,
'latitude' => true,
'longitude' => true,
'bearing' => true,
'preload' => true,
'agtheme' => true,
'agpoiaudios' => true,
'agpoiimages' => true,
'agpoitextes' => true,
'agpoiwaypoints' => true,
];
protected function _getImagesString()
{
if (isset($this->_properties['images_string'])) {
return $this->_properties['images_string'];
}
if (empty($this->agpoiimages)) {
return '';
}
$agpoiimages = new Collection($this->agpoiimages);
$str = $agpoiimages->reduce(function ($string, $agpoiimage)
{
return $string . $agpoiimage->image . ', ';
}, '');
return trim($str, ', ');
}
protected function _getAudiosString()
{
if (isset($this->_properties['audios_string'])) {
return $this->_properties['audios_string'];
}
if (empty($this->agpoiaudios)) {
return '';
}
$agpoiaudios = new Collection($this->agpoiaudios);
$str = $agpoiaudios->reduce(function ($string, $agpoiaudio)
{
return $string . $agpoiaudio->filename . ', ';
}, '');
return trim($str, ', ');
}
protected function _getParagraphLengthString() {
if (isset($this->_properties['paragraph_length_string'])) {
return $this->_properties['paragraph_length_string'];
}
if (empty($this->agpoitextes)) {
return '';
}
$agpoitextes = new Collection($this->agpoitextes);
$str = $agpoitextes->reduce(function ($string, $agpoitexte) {
return $string . str_word_cound($agpoitexte->paragraph) . __(" mots<br/>");
}, '');
return trim($str, '<br/>');
}
}
Unfortunately, I doubt that the problem could come from the code above, I better think about of a stupid mistake somewhere else but I really wonder what could cause such problem and here is my question:
Could anyone tell me what allow cake to look for a getter?
What could prevent cake to call a getter??
I guess the answer is not obvious for the last question but any hint would be appreciated...
EDIT
#ndm
debug(get_class($agPoi));
'Cake\ORM\Entity'
whereas
debug(get_class($agTheme));
'App\Model\Entity\Agtheme'
so obviously Agpoi is not well defined but I don't see why...
EDIT 2
Table is (AgpoisTable.php):
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Agpois Model
*/
class AgpoisTable extends Table {
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config) {
$this->table('agpois');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsToMany('Agthemes', [
'foreignKey' => 'agpoi_id',
'targetForeignKey' => 'agtheme_id',
'joinTable' => 'agthemes_agpois',
'dependent' => true,
]);
$this->hasMany('Agpoiaudios', [
'dependent' => true,
'foreignKey' => 'agpoi_id',
]);
$this->hasMany('Agpoiimages', [
'dependent' => true,
'foreignKey' => 'agpoi_id',
]);
$this->hasMany('Agpoitextes', [
'dependent' => true,
'foreignKey' => 'agpoi_id',
]);
$this->hasMany('Agpoiwaypoints', [
'dependent' => true,
'foreignKey' => 'agpoi_id',
]);
}
public function validationDefault(Validator $validator) {
$validator
....
return $validator;
}
}
I also made a search for 'agpoi' everywhere in case.... no unexpected other agpoi classname or something like that.
debug($this->Agpois);
object(App\Model\Table\AgpoisTable) {
'registryAlias' => 'Agpois',
'table' => 'agpois',
'alias' => 'Agpois',
'entityClass' => '\Cake\ORM\Entity',
'associations' => [
(int) 0 => 'agthemes',
(int) 1 => 'agpoiaudios',
(int) 2 => 'agpoiimages',
(int) 3 => 'agpoitextes',
(int) 4 => 'agpoiwaypoints',
(int) 5 => 'agthemesagpois'
],
'behaviors' => [],
'defaultConnection' => 'default',
'connectionName' => 'default'
}
so always the problem of entityClass of course.
The problem here seems to be name of the table class, respetively the name of the entity class, probably depends on your perspective.
By default, the name of the corresponding entity class is guessed by using the singular table class name without the trailing Table, however Agpois doesn't inflect to Agpoi as you might have assumed, but just Agpois, and so it's looking for the class App\Model\Entity\Agpois, which doesn't exist, hence the fallback to the default entity base class.
I'm not sure what Agpois means, and which language it stems from, but it's certainly not english, and thus such problems aren't unexpected, as the Inflector is ment to handle english words only.
tl;dr
If you must use the name Apgois, then you'll have to either define the name of the entity class manually in your tables initialize() method:
public function initialize(array $config)
{
// ...
$this->entityClass('Apgoi');
// ...
}
or configure the inflector so that it can handle that word:
Inflector::rules('irregular', ['apgoi' => 'apgois']);
or even rename the entity class to Apgois if that is applicable and doesn't cause any naming conflicts.
See also
Cookbook > ORM > Table Objects > Customizing the Entity Class a Table Uses
Cookbook > Inflector > Loading Custom Inflections

Saving associated models in Cakephp 3

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.

Resources