Saving entity, with table associated to itself through join table - cakephp

I'm building an App with CakePHP 3.0. I have a FactsTable and an InterferencesTable. Interferences has this fields:
[id][changed_fact_id][influenced_fact_id][trend][modified_by][modified_at][created_by][created_at]
associations look like this:
FactsTable:
`$this->belongsToMany('InfluencedFacts', [
'through' => 'Interferences',
'className' => 'Facts',
'foreignKey' => 'changed_fact_id'
]);
$this->belongsToMany('ChangedFacts', [
'through' => 'Interferences',
'className' => 'Facts',
'foreignKey' => 'influenced_fact_id'
]);`
InterferencesTable:
`$this->belongsTo('ChangedFacts', [
'className' => 'Facts',
'foreignKey' => 'influenced_fact_id',
]);
$this->belongsTo('InfluencedFacts', [
'className' => 'Facts',
'foreignKey' => 'changed_fact_id',
]);`
I baked my controllers and views. Saving a new Fact works fine but the association isn't saved. I tried to save the association manually but it doesn't work, too.
If i made a mistake in model-association, please tell me ;)
saving code from FactsController.php:
`public function add() {
$fact = $this->Facts->newEntity($this->request->data);
if ($this->request->is('post')) {
if ($this->Facts->save($fact)) {
$id = $fact->get('id');
$this->Flash->success('The fact has been saved.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('The fact could not be saved. Please, try again.');
}
}
$aggregates = $this->Facts->Aggregates->find('list');
$plants = $this->Facts->Plants->find('list');
$influencedFacts = $this->Facts->InfluencedFacts->find('list');
$this->set(compact('fact', 'aggregates', 'plants', 'influencedFacts'));
}`
Fact Entity:
`protected $_accessible = [
'name' => true,
'short' => true,
'description' => true,
'modified_by' => true,
'modified_at' => true,
'created_by' => true,
'created_at' => true,
'aggregates' => true,
'plants' => true,
];`

Related

Why does patchEntity() cut out image information? (CakePHP3)

Using the POST method $data = $this->request->getData(); , I get the archive:
[
'category_id' => '62',
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => '30',
'is_new' => '1',
'img' => [
'tmp_name' => 'D:\Web\OpenServer\userdata\temp\php70D9.tmp',
'error' => (int) 0,
'name' => 'IronMan.jpg',
'type' => 'image/jpeg',
'size' => (int) 131830
]
]
By preparing these data for the record in the database:
$product = $this->Products->patchEntity($product, $data);
But the patchEntity() method cuts out all the information about the image.
I get:
object(App\Model\Entity\Product) {
'category_id' => (int) 62,
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => (float) 30,
'is_new' => (int) 1,
'img' => '', // <--- empty :(
'[new]' => true,
'[accessible]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'img' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true
],
'[dirty]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'img' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
It can be fixed? Tell me at least about. Thank you.
When patching/creating an entity, the data is bein marshalled according to the respective columns data type, as you can see for other properties like price, which is converted from a string to a float.
Your img column is probably of the type string, causing the marshaller to convert the data accordingly (see \Cake\Database\Type\StringType::marshal()).
There are various ways to avoid that, for example using a different property name that doesn't map to an existing column, like img_upload, and then after moving the upload, manually set the resulting filesystem path to the img property and save that.
That could also be done in the beforeMarshal event in your ProductsTable class, so that the view template can continue to use the img property:
public function beforeMarshal(
\Cake\Event\Event $event,
\ArrayObject $data,
\ArrayObject $options
) {
if (isset($data['img'])) {
$data['img_upload'] = $data['img'];
unset($data['img']);
}
}
You could also create a custom database type for the img column, one which doesn't marshal the data to a string, but just passes it on:
namespace App\Database\Type;
use Cake\Database\Type;
class FileType extends Type
{
public function marshal($value)
{
return $value;
}
}
You'd have to assign the filesystem path anyways though, you'd basically just avoid using a separate/temporary property.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
I do not know how much this is correct, but in the end I did the following and everything works as I need:
In ProductsController:
public function add()
{
$product = $this->Products->newEntity();
if ($this->request->is('post')) {
$data = $this->request->getData();
$product = $this->Products->patchEntity($product, $data);
// If there is a picture that we checked (by the method of validationDefault, when calling patchEntity) and have already uploaded to the server in a temporary folder, then
if($product->img_upload['name']){
// We call the user method of processing the downloaded image
$product = $this->_customUploadImg($product);
// Leave only the name of the new file, adding it to the new property, with the name corresponding to the name of the table in the database
$product->img = $product->img_upload['name'];
}
// Delete an unnecessary property
unset($product->img_upload);
if ($this->Products->save($product)) {
// ...
}
// ...
}
In Product.php:
class Product extends Entity{
protected $_accessible = [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true,
// 'img' => true,
// Here we specify not 'img' as in the database table, but 'img_upload', in order for ProductsController not to delete our data file about the uploaded file when patchEntity was called.
'img_upload' => true,
];
}
In ProductsTable.php:
public function validationDefault(Validator $validator)
{
//...
$validator
->allowEmpty('img_upload', 'create')
->add('img_upload', [
'uploadError' => [
'rule' => 'uploadError',
'message' => 'Error loading picture'
],
'mimeType' => [
'rule' => ['mimeType', ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']],
'message' => 'Only image files are allowed to be uploaded: JPG, PNG и GIF'
],
'fileSize' => [
'rule' => ['fileSize', '<=', '2MB'],
'message' => 'The maximum file size should be no more than 2 MB'
]
]);
//...
}
In add.ctp:
echo $this->Form->create($product, ['type' => 'file']) ?>
// ...
echo $this->Form->control('img_upload', ['type' => 'file', 'label'=>'Product photo']);
//...
Thanks "ndm" and "mark"!

Superuser or Admin in Cakephp 3 - E-Commerce with Admin

I'm creating an E-Commerce website using CakePHP 3
I need to create an Admin page that will allow the Admin to upload
products and possibly view a few KPI's etc..
Is there a way in Cake to have a User (general customer shopping on the site) and a Superuser (or Admin) at the same time? I have an 'is_admin' column in my Users table to differentiate between admin and user. Do I just need to have something like this in my addProducts function or is there a better way?:
public function addProducts(){
$user = $this->Auth->user();
if($user['is_admin']) {
//allow access
} else {
//throw anauthorised exception
}
}
Thanks in advance
You can manage it via different URL's for admin and front User. This can be managed via the routes and the APP Controller.
What I am using for one of my appplication is as below:
In the routes.php file
Router::prefix('admin', function ($routes) {
// All routes here will be prefixed with `/admin`
// And have the prefix => admin route element added.
$routes->fallbacks('DashedRoute');
$routes->connect('/', array('controller' => 'Users', 'action' => 'login'));
/* Here you can define all the routes for the admin */
});
Router::scope('/', function ($routes) {
$routes->connect('/', array('controller' => 'Users', 'action' => 'login', 'home'));
/* Here you can define all the routes for the frontend */
});
Please note for the Admin you need to create a directory in all /src/Controller, /src/Template named as "Admin" and within these directories you can use the same structure that we use in our code.
Now comes the code that needs to be written in /src/Controller/AppController.php
public $prefix = '';
public function initialize()
{
$this->prefix = (!empty($this->request->params['prefix'])?$this->request->params['prefix']:'');
$this->set('prefix',$this->prefix);
if( !empty($this->prefix) && $this->prefix==='admin' )
{
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Users',
'action' => 'login',
'prefix'=>'admin'
],
'loginRedirect' => [
'controller' => 'Users',
'action' => 'index',
'prefix'=>'admin'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login',
'prefix'=>'admin'
],
'authError' => 'Did you really think you are allowed to see that?',
'authenticate' => [
'Form' => [
'finder' => 'admin',
'fields' => ['username' => 'email', 'password' => 'password']
]
],
'storage' => ['className' => 'Session', 'key' => 'Auth.Admin']
]);
}
else
{
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
'loginRedirect' => [
'controller' => 'Users',
'action' => 'myaccount'
],
'logoutRedirect' => [
'controller' => 'Users',
'action' => 'login'
],
'authError' => 'Did you really think you are allowed to see that?',
'authenticate' => [
'Form' => [
'finder' => 'user',
'fields' => ['username' => 'email', 'password' => 'password']
]
],
'storage' => ['className' => 'Session', 'key' => 'Auth.User']
]);
}
}
Here you can see that we are using different keys for the storage Auth.User and Auth.Admin
For the finder you need to write the below code in your user model table located at src\Model\Table\UsersTable.php
public function findAdmin(\Cake\ORM\Query $query, array $options)
{
$query
->select(array('Users.email', 'Users.password','Users.id','Users.role_id'))
->where(array('Users.role_id' => 1));
return $query;
}
public function findUser(\Cake\ORM\Query $query, array $options)
{
$query
->select(array('Users.email', 'Users.password','Users.id','Users.role_id'))
->where(array('Users.status' => 1,'Users.role_id' => 3));
return $query;
}
Note, Here I am keeping role_id "1" for Admin and "3" for front Users.
In this manner, even you can set the login for both in the same browser as key for both the user types is different.
Hope this helps you setup the structure accordingly.

cakePHP 3: multi JOIN

I'm having trouble with relationships between tables with CakePHP 3.x. I am struggling to make realcionamento between tables with more than 2 levels realcionamento .
I will introduce the code of relationships and also the query I'm doing to make it more clear what behavior relations:
class CircuitosTable extends Table{
public function initialize(array $config)
{
$this->table('circuitos');
$this->addAssociations([
'belongsTo' => [
'Planostreinos' => [
'foreignKey' => 'id_plano_treino',
'joinType' => 'INNER',
'bindingKey' => 'id'
]
],
'hasMany' => [
'Atividades' => [
'className' => 'Atividades',
'foreignKey' => 'id_circuito',
'bindingKey' => 'id',
'joinType' => 'INNER',
'dependent' => false,
'cascadeCallbacks' => false,
'propertyName' => '_atividades'
],
]
]);
}
}
class AtividadesTable extends Table{
public function initialize(array $config)
{
$this->table('atividades');
$this->addAssociations([
'belongsTo' => [
'Tiposexercicios' => [
'foreignKey' => 'id_tipo_exercicio',
'joinType' => 'INNER',
'bindingKey' => 'id'
],
'Circuitos' => [
'foreignKey' => 'id_circuito',
'joinType' => 'INNER',
'bindingKey' => 'id'
]
]
]);
}
}
class TiposexerciciosTable extends Table{
public function initialize(array $config)
{
$this->table('tipos_exercicios');
$this->addAssociations([
'hasMany' => [
'Atividades' => [
'className' => 'Atividades',
'foreignKey' => 'id_tipo_exercicio',
'bindingKey' => 'id',
'joinType' => 'INNER',
'dependent' => false,
'cascadeCallbacks' => false,
'propertyName' => '_atividades'
]
]
]);
}
}
I'm doing this query:
$circuitos = $CircuitosTable->find('all', [
'conditions' => ['id_plano_treino' => $idPlano],
'contain' => ['Atividades'],
'joins' => [
[
"table" => "Tiposexercicios",
"alias" => "TipoExercicio",
"type" => "INNER",
"conditions" => ["TipoExercicio.id = Atividades.id_tipo_exericio"]
]
]
]);
The expected result:
Select * From Circuitos
JOIN Atividades on Atividades.id_circuito = Circuitos.id
JOIN Tiposexercicios on Tiposexercicios.id = Atividades.id_tipo_exercicio
The submitted query does not work properly. How should do?
hasMany associations are being queried using a separate query, so using contain() to include Atividades will not include the table in the main query.
It looks like you want to filter by associated data, so you can save yourself a lot of trouble by simply using Query::matching() or Query::innerJoinWith(). This will join in associated tables as needed.
The following would create a query similar to what you seem to be looking for:
$circuitos = $CircuitosTable
->find()
->innerJoinWith('Atividades.Tiposexercicios')
->where([
'Circuitos.id_plano_treino' => $idPlano
])
->group('Circuitos.id');
SELECT
-- ...
FROM
circuitos Circuitos
INNER JOIN atividades Atividades
ON Circuitos.id = (Atividades.id_circuito)
INNER JOIN tipos_exercicios Tiposexercicios
ON Tiposexercicios.id = (Atividades.id_tipo_exercicio)
WHERE
Circuitos.id_plano_treino = 123
GROUP BY
Circuitos.id
The grouping is required as you may otherwise receive duplicate results.
See also Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data
On a side note, the table key in the join definition is ment to hold the actual database table name, which, according to your table classes, is tipos_exercicios!

Test add() method with CakePHP 3 and PHPUnit doesn't work

I used bake to generate the controller of my users table, now I need to test using PHPUnit and CakePHP 3 if a record has been entered in the database but I can not see if this actually being inserted.
I can not apply this solution: Question
First error:
1) App\Test\TestCase\Controller\UsersControllerTest::testAdd Failed
asserting that '' contains "The user has been saved.".
If I remove the assertion (or change to assertResponseSuccess)
$this->assertResponseContains('The user has been saved.');
in assertEquals the array $result is empty.
add Action in UsersController:
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->data);
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
}
$userTypes = $this->Users->UserTypes->find('list', ['limit' => 200]);
$this->set(compact('user', 'userTypes'));
$this->set('_serialize', ['user']);
}
testAdd in UsersControllerTest:
public function testAdd()
{
$this->get('/users/add');
$this->assertResponseOk();
//-------------------------------------------------------------------------
$data = [
'id' => 999999,
'email' => 'usuariocomum999999#gmail.com',
'password' => 'usuariocomum999999senha',
'username' => 'usuariocomum999999username',
'user_type_id' => 900000,
'created' => '2014-07-17 18:46:47',
'modified' => '2015-07-17 18:46:47'
];
$this->post('/users/add', $data);
$this->assertResponseContains('The user has been saved.');
//-------------------------------------------------------------------------
$expected = [
[
'id' => 999999,
'email' => 'usuariocomum999999#gmail.com',
'password' => 'usuariocomum999999senha',
'username' => 'usuariocomum999999username',
'user_type_id' => 900000,
'created' => new Time('2014-07-17 18:46:47'),
'modified' => new Time('2015-07-17 18:46:47')
]
];
$users = TableRegistry::get('Users');
$query = $users->find('all', [
'fields' => ['Users.id', 'Users.email', 'Users.password',
'Users.username', 'Users.user_type_id', 'Users.created',
'Users.modified'],
'conditions' => ['Users.id' => 999999]
]);
$result = $query->hydrate(false)->toArray();
$this->assertEquals($expected, $result);
}
Datasource test:
'test' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Mysql',
'persistent' => false,
'host' => 'localhost',
//'port' => 'nonstandard_port_number',
'username' => 'shop',
'password' => 'shop',
'database' => 'shoppingtest',
'encoding' => 'utf8',
'timezone' => 'UTC',
'cacheMetadata' => true,
'quoteIdentifiers' => false,
//'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
]
Note: CakePHP 3.0.11 and PHPUnit 4.8.6
You need to test that there was a redirect and a success message in the Session, The response will not contain your text as the response is just a redirect header code for the browser.

Test delete() method with CakePHP 3 and PHPUnit doesn't work

I used bake to generate the controller of my users table, now I need to test using PHPUnit and CakePHP 3 if a record has been deleted.
NO error was prompted, just failure in test.
I can not apply this solution: Solution
testDelete in UsersControllerTest:
public function testDelete()
{
$this->get('/users/delete/900000');
$users = TableRegistry::get('Users');
$query = $users->find('all', [
'fields' => ['Users.id', 'Users.email', 'Users.password',
'Users.username', 'Users.user_type_id', 'Users.created',
'Users.modified'],
'conditions' => ['Users.id' => 900000]
]);
$result = $query->hydrate(false)->toArray();
$this->assertEmpty($result);
}
delete method in UsersController:
public function delete($id = null)
{
$user = $this->Users->get($id);
if ($this->Users->delete($user)) {
$this->Flash->success(__('The user has been deleted.'));
} else {
$this->Flash->error(__('The user could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
Datasource test:
'test' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Mysql',
'persistent' => false,
'host' => 'localhost',
//'port' => 'nonstandard_port_number',
'username' => 'shop',
'password' => 'shop',
'database' => 'shoppingtest',
'encoding' => 'utf8',
'timezone' => 'UTC',
'cacheMetadata' => true,
'quoteIdentifiers' => false,
//'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
]
Note: CakePHP 3.0.11 and PHPUnit 4.8.6
Try that:
$query = $users->find('all', [
'fields' => ['Users.id', 'Users.email', 'Users.password',
'Users.username', 'Users.user_type_id', 'Users.created',
'Users.modified'],
'conditions' => ['Users.id' => 900000]
]);
$result = $query->hydrate(false)->toArray();
$result_count = count($result);
$expected_count = 0;
$this->assertEquals($expected_count, $result_count);

Resources