I want to have some form-fields encrypted when they are added/edited and decrypted when they are looked up by cake. Here is the code that works for me in v2.7.2:
<
Edit: @npm was right about the virtual properties not working. Now i'm angry at myself for giving a bad answer. serves me right for not checking it before I posted.
To make it right, I've implemented a version using behaviors to decrypt the fields as they are read, and encrypt them as they are written to the database.
Note: This code does not currently incorporate any custom finders, so it will not support searching by the encrypted field.
eg.
$this->Patient->findByPatientFirstname('bob'); // this will not work
Behavior
/src/Model/Behavior/EncryptBehavior.php
'YOUR_KEY_KERE', /* set them in the EntityTable, not here */
'fields' => []
];
/**
* Before save listener.
* Transparently manages setting the lft and rght fields if the parent field is
* included in the parameters to be saved.
*
* @param \Cake\Event\Event $event The beforeSave event that was fired
* @param \Cake\ORM\Entity $entity the entity that is going to be saved
* @return void
* @throws \RuntimeException if the parent to set for the node is invalid
*/
public function beforeSave(Event $event, Entity $entity)
{
$isNew = $entity->isNew();
$config = $this->config();
$values = $entity->extract($config['fields'], true);
$fields = array_keys($values);
$securityKey = $config['key'];
foreach($fields as $field){
if( isset($values[$field]) && !empty($values[$field]) ){
$entity->set($field, Security::encrypt($values[$field], $securityKey));
}
}
}
/**
* Callback method that listens to the `beforeFind` event in the bound
* table. It modifies the passed query
*
* @param \Cake\Event\Event $event The beforeFind event that was fired.
* @param \Cake\ORM\Query $query Query
* @param \ArrayObject $options The options for the query
* @return void
*/
public function beforeFind(Event $event, Query $query, $options)
{
$query->formatResults(function ($results){
return $this->_rowMapper($results);
}, $query::PREPEND);
}
/**
* Modifies the results from a table find in order to merge the decrypted fields
* into the results.
*
* @param \Cake\Datasource\ResultSetInterface $results Results to map.
* @return \Cake\Collection\Collection
*/
protected function _rowMapper($results)
{
return $results->map(function ($row) {
if ($row === null) {
return $row;
}
$hydrated = !is_array($row);
$fields = $this->_config['fields'];
$key = $this->_config['key'];
foreach ($fields as $field) {
$row[$field] = Security::decrypt($row[$field], $key);
}
if ($hydrated) {
$row->clean();
}
return $row;
});
}
}
Table
/src/Model/Table/PatientsTable.php
table('patients');
$this->displayField('id');
$this->primaryKey('id');
// will encrypt these fields automatically
$this->addBehavior('Encrypt',[
'key' => Configure::read('Security.key'),
'fields' => [
'patient_surname',
'patient_firstname'
]
]);
}
}
I feel your pain. the ORM layer in cakephp 3 is radically different from cake2. They split the entity model and the table ORM into two different classes, and afterFind has been removed. I would take a look at using virtual properties. I think it might be suitable for your use case.
Example below.
set('patient_surname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _setPatientFirstname($str)
{
$this->set('patient_firstname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _getPatientSurname()
{
return Security::decrypt($this->patient_surname, Configure::read('Security.key'));
}
protected function _getPatientFirstname()
{
return Security::decrypt($this->patient_first_name, Configure::read('Security.key'));
}
}