Cakephp api POST request , saving data without validation - cakephp

I have created a route with Api prefix
$routes->scope('/api', function (RouteBuilder $routes) {
$routes->setExtensions(['json']);
$routes->post(
'/add-categories',
['controller' => 'Miles', 'action' => 'addCategories','prefix'=>'Api']
);
}
I have created a controller file in directory Controller/Api/MilesController.php
I have created a addCategory method like below
public function addCategories()
{
$categoriesTable = $this->fetchTable('Categories');
$categoryEnt = $categoriesTable->newEmptyEntity();
if ($this->request->is('post')) {
$category = $categoriesTable->patchEntity($categoryEnt, $this->request->getData());
if ($categoriesTable->save($category)) {
$responseBody = [
'status' => 201,
'data' => $category
];
}else{
$responseBody = [
'status' => 400,
'data' => $category->getErrors()
];
}
}
$this->set(compact('responseBody'));
$this->viewBuilder()->setOption('serialize', ['responseBody']);
}
In postman if I try to send a post request without any data , then I getting the response like below
{
"responseBody": {
"status": 201,
"data": {
"created": "2022-05-10T20:04:13+09:00",
"modified": "2022-05-10T20:04:13+09:00",
"id": 8
}
}
}
My Model/Table/CategoriesTable.php looks like
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class CategoriesTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('categories');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->hasMany('Products', [
'foreignKey' => 'category_id',
]);
}
public function validationDefault(Validator $validator): Validator
{
$validator
->allowEmptyString('id', null, 'create');
$validator
->scalar('title')
->notEmptyString('title');
$validator
->boolean('status')
->notEmptyString('status');
$validator
->scalar('imageUrl')
->allowEmptyFile('imageUrl');
return $validator;
}
}
Why my notEmptyString validation not working which I have written in CategoriesTable.php file ?
Note : This Api/MilesController.php has no self model. How can I apply CategoriesTable validation here ?

None of your fields are set as being required, ie they are all optional, and when an optional field is missing, no validation rules will be applied.
So for all fields that must be present, use requirePresence(), for example:
$validator
->requirePresence('title', 'create') // required on create, optional on update
->scalar('title')
->notEmptyString('title');
See also
Cookbook > Validation > Creating Validators > Requiring Field Presence

Related

Cakephp authentication plugin how can I implement login in admin prefix?

I'm trying to implement cakephp authentication plugin in admin prefix
Route I have written for admin prefix
$routes->prefix('admin', function (RouteBuilder $routes) {
$routes->connect('/',['controller'=>'AdminUsers','action'=>'login']);
$routes->fallbacks(DashedRoute::class);
});
In application.php , I have followed everything That mentioned in authentication documentation
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$authenticationService = new AuthenticationService([
'unauthenticatedRedirect' => Router::url('/admin'),
'queryParam' => 'redirect',
]);
// Load identifiers, ensure we check email and password fields
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'email',
'password' => 'password',
]
]);
// Load the authenticators, you want session first
$authenticationService->loadAuthenticator('Authentication.Session');
// Configure form data check to pick email and password
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'email',
'password' => 'password',
],
'userModel' => 'AdminUsers',
'loginUrl' => Router::url('/admin'),
]);
return $authenticationService;
}
I have changed users model to AdminUsers for database table admin_users
Now in admin/appController.php
I have loadComponent in initialize method
$this->loadComponent('Authentication.Authentication');
In before filter method I have added
$this->Authentication->allowUnauthenticated(['login']);
Now after submit login form I am getting error
Table class for alias Users could not be found.
In getAuthenticationService method I have changed model Users to AdminUsers. Why it's going for Users model rather then AdminUsers model ?
My Table/AdminUsersTable.php table class look likes
<?php
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class AdminUsersTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('admin_users');
--------
After ndm comment I am able to change default model in application.php , But how can I change it in Admin/AppController.php ? Code that I have tried.
public function beforeFilter(EventInterface $event)
{
$service = new AuthenticationService();
$service->loadIdentifier('Authentication.Password', [
'resolver' => [
'className' => 'Authentication.Orm',
'userModel' => 'AdminUsers',
],
]);
$this->Authentication->allowUnauthenticated(['login','signup']);
$this->viewBuilder()->setLayout('admin');
}

Pass nested array to Symfony Form for EAV type table

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.

cakephp event listener not found

I'm using Cakephp 3.2 and proffer plugin to upload images.
By default the path of the image is as follows
/media/files/<tablename>/<primary_key>/<filename>
Each time a new row is inserted into same table a new folder is created by its primary key.
I want to upload all images of a table to the same directory. means path like
/media/files/<tablename>/<filename>
I'm using event listener as per given in proffer documentation.
This is my SellersTable.php
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Event\Event;
class SellersTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$listener = new App\Event\UploadFileNameListener(); // line 23
$this->eventManager()->on($listener);
$this->table('sellers');
$this->displayField('id');
$this->primaryKey('id');
$this->addBehavior('Timestamp');
$this->addBehavior('Proffer.Proffer', [
'profile_picture' => [
'root' => Configure::read('ArgoSystems.media.upload') . DS . 'files',
'dir' => 'dir'
]
]);
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->requirePresence('first_name', 'create')
->notEmpty('first_name');
$validator
->requirePresence('last_name', 'create')
->notEmpty('last_name');
$validator
->email('email')
->requirePresence('email', 'create')
->notEmpty('email')
->add('email', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
$validator->provider('proffer', 'Proffer\Model\Validation\ProfferRules');
$validator
->add('profile_picture', 'proffer', [
'rule' => ['dimensions', [
'min' => ['w' => 100, 'h' => 500],
'max' => ['w' => 100, 'h' => 500],
]],
'message' => 'Image must be of 100 x 500 resolution',
'provider' => 'proffer'
])
->requirePresence('profile_picture', 'create')
->allowEmpty('profile_picture','update');
$validator
->requirePresence('password', 'create')
->notEmpty('password');
return $validator;
}
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['email']));
return $rules;
}
}
and created UploadFileNameListener.php in src/Event/
<?php
namespace App\Event;
use Cake\Event\Event;
use Cake\Event\EventListenerInterface;
use Cake\Utility\Inflector;
use Proffer\Lib\ProfferPath;
class UploadFileNameListener implements EventListenerInterface
{
public function implementedEvents()
{
return [
'Proffer.afterPath' => 'change',
];
}
/**
* Rename a file and change it's upload folder before it's processed
*
* #param Event $event The event class with a subject of the entity
* #param ProfferPath $path
* #return ProfferPath $path
*/
public function change(Event $event, ProfferPath $path)
{
// Detect and select the right file extension
switch ($event->subject()->get('image')['type']) {
default:
case "image/jpeg":
$ext = '.jpg';
break;
case "image/png":
$ext = '.png';
break;
case "image/gif":
$ext = '.gif';
break;
}
// Create a new filename using the id and the name of the entity
$newFilename = $event->subject()->get('id') . '_' . Inflector::slug($event->subject()->get('name')) . $ext;
// set seed
$path->setSeed('profile_picture');
// Change the filename in both the path to be saved, and in the entity data for saving to the db
$path->setFilename($newFilename);
$event->subject('image')['name'] = $newFilename;
// Must return the modified path instance, so that things are saved in the right place
return $path;
}
}
But this is giving Fatal error as
Error: Uncaught Error: Class 'App\Model\Table\App\Event\UploadFileNameListener' not found in
/var/www/html/projects/admin/src/Model/Table/SellersTable.php:23
From the error message, it's clear that it's trying to load the class with a namespace relative to the namespace of your current class. Try
$listener = new \App\Event\UploadFileNameListener();

CakePHP 3.0 - Login not working

I've been following the CakePHP 3.0 tutorials (Bookmarker & Blog) regarding the Login/Authentication sections and applying it to my own code, but I can't seem to get it working properly.
I'm able to see all indexes without even being logged in. Logging into the system does not work; the login page just refreshes.
EDIT: Okay, seems like my test users were duds, I made a new user and it can login. However, I still have the issue of non-users being able to see indexes of tables (they can't create any variables in any of the tables apart from User, which has been allowed specifically).
EDIT2: I fixed it by removing the beforeFilter function in AppController.
User Controller:
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event;
class UsersController extends AppController
{
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow(['add', 'logout']);
}
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error(__('Incorrect username or password, please try again.'));
}
}
public function logout()
{
$this->Flash->success('You are now logged out.');
return $this->redirect($this->Auth->logout());
}
public function index()
{
$this->set('users', $this->Users->find('all'));
}
public function view($id)
{
$user = $this->Users->get($id);
$this->set(compact('user'));
}
}
App Controller:
<?php
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Event\Event;
class AppController extends Controller
{
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginRedirect' => [
'controller' => 'Users',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
}
}
User Table:
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class UsersTable extends Table
{
public function validationDefault(Validator $validator)
{
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->requirePresence('username', 'create')
->notEmpty('username', 'A username is required');
$validator
->requirePresence('password', 'create')
->notEmpty('password', 'A password is required');
$validator
->email('email')
->requirePresence('email', 'create')
->notEmpty('email', 'An email is required');
$validator
->requirePresence('role', 'create')
->notEmpty('role', 'inList', [
'rule' => ['inList', ['admin', 'artist', 'engineer']],
'message' => 'Please enter a valid role'
]);
return $validator;
}
}
I managed to fix it by removing the beforeFilter function in AppController.
public function beforeSave(Event $event)
{
$entity = $event->data['entity'];
// Make a password for digest auth.
$entity->digest_hash = DigestAuthenticate::password(
$entity->username,
$entity->password,
env('SERVER_NAME')
);
return true;
}

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