I have a model (Addresses) with the following fields:
id: char(36) #using uuid
name: varchar(150)
#city, state, etc here
created: timestamp
modified: timestamp
In my AddressesTable class I have:
public function initialize(array $config)
{
$this->table('addresses');
$this->addBehavior('Timestamp');
}
In my controller, I have this in the edit method:
public function edit($id = null) {
$addressesTable = TableRegistry::get('Addresses');
$address = $addressesTable->get($id);
if ($this->request->is(array('post','put'))) {
$address = $addressesTable->patchEntity($address, $this->request->data);
if ($addressesTable->save($address)) {
$this->Flash->success('The address has been saved.');
return $this->redirect(['action' => 'addresses']);
} else {
$this->Flash->error('The address could not be saved. Please, try again.');
}
}
$this->set(compact('address'));
}
The problem I'm running into is this. According to everything I've read, this should 'update' the record (which it does), and update the 'modified' field in the DB to the current timestamp (which it also does). However, in addition, it also updates the created timestamp (which, it should NOT do).
What am I missing here?
I need this to update ONLY the modified column and NOT the created column on save.
I had the same problem using CakePHP 3.4. I solved using this in my Model/Table:
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
}
More informations here: https://book.cakephp.org/3.0/en/tutorials-and-examples/blog/part-two.html
Related
I use CakePHP 4 with a plugin called softdelete trait (the forked version for cake 4).
I have a simple many-to-many relation between virtual classrooms and learners.
Furthermore, I made a pivot table called learners_virtual_rooms to link the two.
Now when I edit my classroom, I can add and remove learners.
The issue comes when I try to delete the classroom while there are still learners linked :
The room is soft deleted, the rows in the pivot table are deleted, so far so good,
and then my learners are deleted too !
(at least try to be deleted because cake targets a column that doesn't exist in the learners table : virtual_room_id)
I tried using dependent options on my relations, removing the softdelete plugin, using hasMany relations to no avail.
Next is my code, feel free to ask more if it's not enough.
The virtual_rooms table
class VirtualRoomsTable extends Table
{
use SoftDeleteTrait;
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('virtual_rooms');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
//[...]
$this->belongsToMany('Learners');
//[...]
}
//[...]
}
The learners table
class LearnersTable extends Table
{
use SoftDeleteTrait;
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('learners');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
// [...]
$this->belongsToMany('VirtualRooms');
// [...]
}
//[...]
}
The delete method in my controller
public function delete(string $virtual_room_id): Response
{
Assert::true($this->request->is(['delete', 'post']), __('Méthode http incorecte'));
$virtual_room = $this->VirtualRooms->get($virtual_room_id);
if ($this->VirtualRooms->delete($virtual_room)) {
return $this->setJsonResponse([
'message' => __('Classe virtuelle supprimée avec succès'),
'success' => true,
]);
}
return $this->setJsonResponse([
'message' => __('La classe virtuelle n\'a pas pu être supprimée'),
'errors' => $virtual_room,
'validationErrors' => $virtual_room->getErrors(),
'success' => false,
]);
}
No file for the pivot table.
The stacktrace of the error look like that :
Cake\Database\Query->execute
ROOT\vendor\salines\cakephp4-soft-delete\src\Model\Table\SoftDeleteTrait.php:120
App\Model\Table\LearnersTable->deleteAll
CORE\src\ORM\Association\BelongsToMany.php:617
Cake\ORM\Association\BelongsToMany->cascadeDelete
CORE\src\ORM\AssociationCollection.php:343
Cake\ORM\AssociationCollection->cascadeDelete
ROOT\vendor\salines\cakephp4-soft-delete\src\Model\Table\SoftDeleteTrait.php:84
App\Model\Table\VirtualRoomsTable->_processDelete
CORE\src\ORM\Table.php:2281
The queries that cause the exception :
This one is ok
DELETE FROM
learners_virtual_rooms
WHERE
virtual_room_id = 31
This one is the culprit
UPDATE
learners
SET
deleted = '2021-03-23 14:44:03'
WHERE
virtual_room_id = '31'
debug($this->VirtualRooms->Learners->junction()) :
APP/Controller\Api\VirtualRoomsController.php (line 119)
object(Cake\ORM\Table) id:0 {
'registryAlias' => 'LearnersVirtualRooms'
'table' => 'learners_virtual_rooms'
'alias' => 'LearnersVirtualRooms'
'entityClass' => 'Cake\ORM\Entity'
'associations' => [
(int) 0 => 'Learners', (int) 1 => 'VirtualRooms',
]
I'm thinking of removing by hand all the learners before deleting the classroom, is it the only way ?
Many thanks for your time and help.
This is my first time creating an API. I want to insert data in two table at the same time.
I have a Listing table and Category table. Relationships of the table is one List => has many Category
Listing columns
id
title
description
Category columns
id
listing_id
category_name
This is my code so far in my controller
public function store(Request $request)
{
return Listings::create($request->all());
}
Listing Model
protected $table = 'testing_table';
public function categories(){
return $this->hasMany('App\Category', 'listing_id', 'id');
}
Category Model
protected $table = 'sample_category';
Routes
Route::post('listings', 'ListingsController#store');
Note: Listing and category will be saved together.
First, you have to make sure each model has the $fillables property set to avoid mass assignment issues. Second, you have to send the category name along with the request.
public function store(Request $request)
{
// You might want to do some validations
$listing = Listings::create($request->all());
$category = $listing->categories()->create([
'category_name' => $request->input('category_name')
]);
// And you can return the date however you see fit.
return response()->json([
'listing' => $listing,
'category' => $category
]);
}
public function store(Request $request)
{
try {
$input = $request->all();
\DB::beginTransaction();
$id = Listing::create($input)->id;
$cat = Category::create([
'listing_id' => $id,
'category_name' => $input['category_name'],
]);
\DB::commit();
return response()->json(['status' => 'success', 'code'=> '200', 'data' => 'as per your requirment'], 200);
} catch (\Throwable $th) {
\DB::rollBack();
echo "<pre>";print_r($th->__toString());die;
}
}
to insert more rows then we have to check another way. Please provide your request data.
Please let me know if this helps
I have just created a very minimal project in CakePHP 4.1, mostly mimicking the CMS tutorial, and want to implement a fairly straightforward piece of logic.
Using the Authorization module I want to allow a user A to be able to view a user B if 1) they are actually the same user (A = B) OR 2) if A is an admin.
There are two DB tables - users and user_types. users has a foreign key user_type_id to user_types.
This relationship is reflected in code as:
##### in UsersTable.php #####
class UsersTable extends Table {
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('users');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->belongsTo('UserTypes');
$this->addBehavior('Timestamp');
}
//...
}
##### in UserTypesTable.php #####
class UserTypesTable extends Table {
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('user_types');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->hasMany('Users');
}
//...
}
In UsersController.php I have:
public function view($id = null)
{
$user = $this->Users->get($id, [
'contain' => ['UserTypes'],
]);
$this->Authorization->authorize($user);
$this->set(compact('user'));
}
And in UserPolicy.php:
use App\Model\Entity\User;
class UserPolicy
{
public function canView(User $user, User $resource)
{
// TODO: allow view if $user and $resource are the same User or if $user is an admin
//
// My problem is that here $user->user_type is NULL
// while $resource->user_type is populated correctly
}
}
The code comment in the above excerpt shows where my problem is.
I do not know how to get $user to have its user_type field populated in order to check whether they're an admin.
As a part of my efforts, I have set the User class to be the authorization identity, following this article: https://book.cakephp.org/authorization/2/en/middleware.html#using-your-user-class-as-the-identity.
Code-wise this looks like:
##### relevant part of Application.php #####
$middlewareQueue
->add(new AuthenticationMiddleware($this))
->add(new AuthorizationMiddleware($this, [
'identityDecorator' => function(\Authorization\AuthorizationServiceInterface $auth, \Authentication\IdentityInterface $user) {
return $user->getOriginalData()->setAuthorization($auth);
}
]));
##### User.php #####
namespace App\Model\Entity;
use Authentication\PasswordHasher\DefaultPasswordHasher;
use Authorization\AuthorizationServiceInterface;
use Authorization\Policy\ResultInterface;
use Cake\ORM\Entity;
/**
* User Entity
*
* #property int $id
* #property string $email
* #property string $password
* #property string|null $name
* #property \App\Model\Entity\UserType $user_type
* #property \Cake\I18n\FrozenTime|null $created
* #property \Cake\I18n\FrozenTime|null $modified
* #property \Authorization\AuthorizationServiceInterface $authorization
*/
class User extends Entity implements \Authorization\IdentityInterface, \Authentication\IdentityInterface
{
protected $_accessible = [
'email' => true,
'password' => true,
'name' => true,
'created' => true,
'modified' => true,
];
/**
protected $_hidden = [
'password',
];
protected function _setPassword(string $password) : ?string
{
if (strlen($password) > 0) {
return (new DefaultPasswordHasher())->hash($password);
}
}
/**
* #inheritDoc
*/
public function can(string $action, $resource): bool
{
return $this->authorization->can($this, $action, $resource);
}
/**
* #inheritDoc
*/
public function canResult(string $action, $resource): ResultInterface
{
return $this->authorization->canResult($this, $action, $resource);
}
/**
* #inheritDoc
*/
public function applyScope(string $action, $resource)
{
return $this->authorization->applyScope($this, $action, $resource);
}
/**
* #inheritDoc
*/
public function getOriginalData()
{
return $this;
}
/**
* Setter to be used by the middleware.
* #param AuthorizationServiceInterface $service
* #return User
*/
public function setAuthorization(AuthorizationServiceInterface $service)
{
$this->authorization = $service;
return $this;
}
/**
* #inheritDoc
*/
public function getIdentifier()
{
return $this->id;
}
}
However, I have not been able to get the identity User in the UserPolicy.php file to have the user_type field populated.
Some under-the-hood magic seems to happen when I call $this->Authorization->authorize() from the controller where I explicitly pass the resource together with its user type (since I have constructed it with 'contain' => ['UserTypes'] BUT the identity user is populated automatically by the Authorization module.
Could someone please help me to find a way to bring associated tables data into the identity user of an authorization policy?
NOTE:
I have fudged the code to make it work like this:
##### in UserPolicy.php #####
use App\Model\Entity\User;
class UserPolicy
{
public function canView(User $user, User $resource)
{
$user = \Cake\Datasource\FactoryLocator::get('Table')->get('Users')->get($user->id, ['contain' => ['UserTypes']]);
// Now both $user->user_type and $resource->user_type are correctly populated
}
}
HOWEVER, this feels awfully "hacky" and not the way it's supposed to be, so my original question still stands.
The identity is being obtained by the resolver of the involved identifier. In case of the CMS tutorial that's the Password identifier which by default uses the ORM resolver.
The ORM resolver can be configured to use a custom finder in case you need to control the query for obtaining the user, that's where you should add the containment for your UserTypes association.
In your UsersTable add a finder like this:
public function findForAuthentication(\Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
return $query->contain('UserTypes');
}
and configure the identifier's resolver to use that finder like this:
$service->loadIdentifier('Authentication.Password', [
'resolver' => [
'className' => 'Authentication.Orm',
'finder' => 'forAuthentication',
],
'fields' => [
'username' => 'email',
'password' => 'password',
]
]);
You need to specify the resolver class name too when overriding the resolver option, as by default it is just a string, not an array that would merge with the new config!
See also
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
Authentication Cookbook > Identifiers
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 have a beforeSave-callback which is called just fine whenever I create a new entity. However when I am editing, it's not called at all.
Could not find anything in the documentation that could be helpful.
Here's my edit function:
public function edit($id = null) {
if (!$id) {
throw new NotFoundException(__('Invalid article'));
}
$article = $this->Articles->get($id);
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->data);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
$this->set('article', $article);
}
The beforeSave function will be triggered only if the data you post/edit is modified.
//triggers only if an entity has been modified
public function beforeSave(Event $event, Entity $entity)
{
if($entity->isNew()) {
//on create
} else {
//on update
}
}
I also had the problem that the tutorial got stuck on this point, but I used the bin/cake bake command to autogenerate the ArticlesTable code and it added this validator:
$validator
->scalar('slug')
->maxLength('slug', 191)
/*->requirePresence('slug', 'create')*/
->notEmptyString('slug')
->add('slug', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);
When I commented the requirePresence() it solved this issue for me. If you have requirePresence('fieldName', 'create') for validation, you will get an error if you don't have that field on a post when creating the new Article entity.
Yes, your need use Event and Entity object:
check this example:
// src/Model/Table/ArticlesTable.php
use Cake\Event\Event;
use Cake\ORM\Entity;
public function beforeSave(Event $event, Entity $entity) {
if ($entity->isNew()) {
return true;
}
// edit code
}
Another way to write the same code:
public function beforeSave(\Cake\Event\Event $event, \Cake\ORM\Entity $entity, \ArrayObject $options){
if($entity->isNew()) {
// on create
} else {
// on update
}
}