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
Related
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.
I have a legacy CRM application that I am migrating from Code Igniter 2.x to Cake 3. I am attempting to implement a Legacy Password Hasher and then move everything to the Cake 3 password hasher.
I have been unable to get authentication to work. As you'll see I've had to deviate from various cake defaults, in particular all tables were created as singular names. Not sure if that's causing any issues. It's been an ordeal. If it matters, Admin exists as a plugin.
AppController
namespace Admin\Controller;
use App\Controller\AppController as BaseController;
class AppController extends BaseController
{
public function initialize() {
parent::initialize();
$this->loadComponent('Auth', [
'loginAction' => '/admin/login', // routes to /admin/user/login
'authenticate' => [
'Form' => [
'passwordHasher' => [
'className' => 'Legacy',
],
'fields' => [
'username' => ['email_address','handle'],
'password' => 'password',
],
'userModel' => 'User',
],
],
'storage' => 'Session'
]);
}
}
UserController
if( $this->request->is('post') ){
$this->loadModel('User');
$me = $this->User->find()->where([
'type IN ' => ['superuser','administrator','user','publisher'],
'status IN ' => ['active','suspended'],
'OR' => [
'email_address' => $this->request->data('username'), 'handle' => $this->request->data('username')
]
])->first();
if( $me->status == 'active' ){
$user = $this->Auth->identify();
if( $user ){
$this->Auth->setUser($user);
if( $this->Auth->authenticationProvider()->needsPasswordRehash() ){
$user = $this->User->get($this->Auth->user('id'));
$user->password = $this->request->data('password');
$this->User->save($user);
}
switch($user->type){
case 'superuser':
$url = '/admin/dashboard/super';
break;
case 'admin':
$url = '/admin/dashboard/';
break;
case 'publisher':
$url = '/admin/dashboard/publisher';
break;
case 'property':
$url = '/admin/dashboard/property';
break;
}
return $this->redirect($url);
}
}
$this->Flash->error(__('Username or password is incorrect'), [
'key' => 'auth'
]);
}
LegacyPasswordHasher
namespace App\Auth;
use Cake\Auth\AbstractPasswordHasher;
class LegacyPasswordHasher extends AbstractPasswordHasher
{
public function hash($password){
$salt_length = 16;
$salt = substr($password, 0, $salt_length);
return $salt . sha1($salt . $password);
}
public function check($password, $hashedPassword){
return $this->hash($password) === $hashedPassword;
}
}
The legacy password hasher does not even seem to be called. I did a work around to force authentication by doing my own check and then setting the auth user data. Still after that when rehashing the password using Cakes DefaultPasswordHasher authentication failed.
I figured this out. There were a few issues here. One, I needed to change the configuration to this:
$this->loadComponent('Auth', [
'loginAction' => '/admin/login',
'authenticate' => [
'Form' => [
'finder' => 'auth',
'passwordHasher' => [
'className' => 'Legacy',
],
'fields' => [
'username' => 'email_address',
'password' => 'password',
],
'userModel' => 'User',
],
],
'storage' => 'Session'
]);
Next, the field I posted needed to be email_address and not username. Last, there was a bug in my implementation of Code Igniters password hasher. It ended up needing to look like this:
public function hash($password){
$salt_length = 16;
$salt = substr($this->salt, 0, $salt_length);
return $salt . sha1($salt . $password);
}
public function check($password, $hashedPassword){
$this->salt = $hashedPassword;
return $this->hash($password) === $hashedPassword;
}
I also needed a custom finder query added to UserTable:
public function findAuth(\Cake\ORM\Query $query, array $options)
{
$query
->select(['id', 'email_address','handle','password'])
->where(['User.status' => 'active']);
return $query;
}
To handle users being able to login with a username or email_address I will just override the the posted value if its not an email address by looking up the user by handle prior to auth doing its thing.
I hope this helps someone.
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]
);
I am using hasOne association.Here my code for UserMastersTable :
class UserMastersTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->table('user_masters');
$this->hasOne('PersonMasters', [
'className' => 'PersonMasters',
'foreign_key' => 'user_master_id',
'conditions' => ['PersonMasters.status' => 1],
'dependent' => true,
]);
} }
When use find() in my controller.It fetch all user_masters data and person_masters data whose status ='1'.
but problem is that i already assign condition where association bind..already give condition that only display that data whose person_masters.status=1.
so why it shows all data of user_masters ?
if i give condition in find() in controller then it works fine..
$this->UserMasters->find('all',
['contain' =>
['PersonMasters'],
'conditions' =>
['PersonMasters.status' => 1]
]);
so, how can i globally give condition that only fetch data of user_masters and person_masters where PersonMasters.status=1?
Try this might be it will resolve your issue
$this->UserMasters->find('all',[
'contain' =>
['PersonMasters' => [
'conditions' => ['status' => 1]
]
],
]);
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.