Cake PHP Version: 3.1.5
I have a problem with saving a hasOne association, which works fine on one table but not with a second.
Ticketsand Cashdrafts are related to Cashpositions in a belongsTo relations. Cashpositions holds two FK for their id. So when a new cashposition is auto-created it holds either a ticket_id or a cashdraft_id. The second FK will be null.
The thing is, that the Tickets-Cashpositions saving is working fine, so everytime a ticket is created a related cashposition is created. But it is not working with Cashdrafts-Cashpositions. I don't understand why, because the setup and relations are exactly the same.
Here is the setup:
class CashpositionsTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Tickets', [
'foreignKey' => 'ticket_id'
]);
$this->belongsTo('Cashdrafts', [
'foreignKey' => 'cashdraft_id'
]);
}
}
class TicketsTable extends Table
{
public function initialize(array $config)
{
$this->hasOne('Cashpositions', [
'foreignKey' => 'ticket_id'
]);
}
}
class CashdraftsTable extends Table
{
public function initialize(array $config)
{
$this->hasOne('Cashpositions', [
'foreignKey' => 'cashdraft_id'
]);
}
}
And then in the controllers add() functions:
class TicketsController extends AppController
{
public function add($memberId = null)
{
$ticket = $this->Tickets->newEntity();
if ($this->request->is('post')) {
$ticket = $this->Tickets->patchEntity($ticket, $this->request->data, [
// working fine: creates new cashposition for this ticket
'associated' => ['Cashpositions']
]);
if ($this->Tickets->save($ticket)) {
$this->Flash->success(__('ticket saved'));
return $this->redirect(['action' => 'view', $ticket->$id]);
} else {
$this->Flash->error(__('ticket could not be saved'));
}
}
class CashdraftsController extends AppController
{
public function add()
{
$cashdraft = $this->Cashdrafts->newEntity();
if ($this->request->is('post')) {
$cashdraft = $this->Cashdrafts->patchEntity($cashdraft, $this->request->data,[
// fail: no associated record created
'associated' => ['Cashpositions']
]);
if ($this->Cashdrafts->save($cashdraft)) {
$this->Flash->success(__('cashdraft saved.'));
return $this->redirect(['action' => 'view', $cashdraft->id]);
} else {
$this->Flash->error(__('cashdraft could not be saved'));
}
}
}
I debugged the $ticket and $cashdraft. But I cannot say I understand the output because:
The array for the ticket will show every related data but no cashposition, although a new record for it was created successfully...
And the array for the new cashdraft where the related cashposition is NOT created will look like this and say "null" for it:
object(App\Model\Entity\Cashdraft) {
'id' => (int) 10,
'amount' => (float) -7,
'created' => object(Cake\I18n\Time) {
'time' => '2015-12-13T20:03:54+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'modified' => object(Cake\I18n\Time) {
'time' => '2015-12-13T20:03:54+0000',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'cashposition' => null, // this part not even showing up for a "ticket" in the debug
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Cashdrafts'
}
In the SQL in DebugKit I can see that for the ticket an INSERT into the related cashpositions table is done. But for cashdrafts there is no INSERT done into the related table. So obviously Cake does not even try to create the associated record.
I'm really out of ideas now! In the database itself both FKs are set up exactly the same, names are correct etc.
Does anybody have an idea what the problem could be or where I could further search for the cause of the second association not working? Thanks!
Ok, so after searching for a million hours I finally realized, that the problem was not with the Model or Controller like I thought. It was (just) the view and the request data not being complete.
Somehow I thought Cake would magically add the entity for the association if non exists even if there is no input for it ;)
In the tickets table for which the saving worked I had an empty input field for a column in Cashpositions that does not even exist anymore and I just hadn't deleted it yet, but it did the trick (don't ask me why).
To fix it now I just put in a hidden input field for the association cashposition.ticket_id and cashposition.cashdraft_id in the add.ctp view for both tables that stays empty. Now the request data contains the array for the association and auto creates a new cashposition with the matching FK every time a new ticket or cashdraft is added.
<!-- Cashdrafts/add.ctp -->
<?php echo $this->Form->input(
'cashposition.cashdraft_id', [
'label' => false,
'type' => 'hidden'
]) ?>
Since I'm just a beginner with this I don't know if this is the best way to go, but it works (finally...)
Related
I have read several Stack Overflow posts and the documentation pages about saving associated data in CakePHP 3, but I can't get my code to work. When creating a new Organisation, I also want to save the data of the new account (NewAccount) that belongs to that Organisation.
Below is a reproducible part of my code. The validation of the Organisations model are executed and if passed, the data is saved. The NewAccounts data does not get saved, and is not even being validated. I have already checked the naming conventions in all of these files, but I can't find anything that might be a problem anymore.
// Model/Table/OrganisationsTable.php
class OrganisationsTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->hasOne('NewAccounts');
}
}
// Model/Table/NewAccountsTable.php
class NewAccountsTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->belongsTo('Organisations', [
'foreignKey' => 'organisation_id'
]);
}
}
// Template/Organisations/admin_add.ctp
echo $this->Form->create($organisation);
echo $this->Form->control('new_account.email', [
'label' => __('Email address'),
'class' => 'form-control',
]);
echo $this->Form->control('new_account.name', [
'label' => __('Name'),
'class' => 'form-control',
]);
echo $this->Form->control('name', [
'label' => __('Organisation name'),
'div' => 'form-group',
'class' => 'form-control',
]);
// Controller/OrganisationsController.php
class OrganisationsController extends AppController
{
public function adminAdd() {
$organisation = $this->Organisations->newEntity();
if($this->request->is('post')) {
$this->Organisations->patchEntity($organisation, $this->request->getData(), [
'associated' => ['NewAccounts']
]);
if ($this->Organisations->save($organisation)) {
$id = $organisation->id;
debug("Success!");
}
else {
debug("Error");
}
}
$this->set(compact('organisation'));
}
}
CakePHP documentation I have referenced:
Saving Data
Form
Associations - Linking Tables Together
Validating Data
Entities
I forgot to make $organisation->new_account accessible:
// Model/Entity/Organisation.php
class Organisation extends Entity
{
protected $_accessible = [
// ...
'new_account' => true,
];
}
By doing this, the field is marked as to be safely assigned.
Entities: Mass Assignment (book) and EntityTrait::$_accessible (API docs)
I was using these codes in my controller to get all the data from my 2 tables and it works fine
$All = Customers::with('order')->paginate(10);
return response()->json([
'code' => 0,
'success' => true,
'data' => $All
], 200);
Here is how I define the relationship between these 2 tables
class Customers extends Model
{
public function order()
{
return $this->hasMany(Orders::class, 'customer_id', 'id');
}
}
class Orders extends Model
{
public function customers()
{
return $this->belongsTo(Customers::class, 'customer_id', 'id');
}
}
Now my desire output is to hide the order id, order timestamps and change the customer_id to customer's name (the customer's name is not in my orders db table).
I'm using 'data' => DataResource::collection($All) in my controller and this is my DataResource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'order' => $this->order
];
}
and of course the output is same with the image above.
My database structure:
orders table:
customer table:
Can anyone help me with that?
The answer is simple and basically a copy of the official documentation. You simply need to wrap your orders in an OrderResource as well.
// DataResource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'order' => OrderResource::collection($this->order)
];
}
// OrderResource
public function toArray($request)
{
return [
'items' => $this->items,
'quantity' => $this->quantity
];
}
I don't really understand why you would want to include the customer_name in your orders when it is already present on the customers object one hierarchy above. But if you really want to add it, you should be able to do so with: 'customer_name' => $this->customers->name.
As a side note: you really should be more consistent with your naming. Why is the resource called DataResource when it is about Customers? Why is your model called Customers in plural form rather than Customer in singular, which is the convention (and more logical if you consider that one model represents one customer). Why is your belongsTo relation called customers() in plural when it returns one customer, while your hasMany relation is called order whereas it returns one or more orders?
I am having two tables. My primary table is Students. And my secondary table is Exams. I am trying to save both the tables using hasMany and belongsToMany Association. But It is saving data in Student table only, not in Exams. Can any one help me to resolve this problem.
Students Model :
class StudentsTable extends Table {
public function initialize(array $config) {
$this->addBehavior('Timestamp');
parent::initialize($config);
$this->table('students');
$this->primaryKey(['id']);
$this->hasMany('Exams', [
'className' => 'Exams',
'foreignKey' => 'student_id',
'dependent'=>'true',
'cascadeCallbacks'=>'true']);
}
}
Exams Model :
class ExamsTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->table('exams');
$this->primaryKey(['id']);
$this->belongsToMany('Students',[
'className'=>'Students',
'foreignKey' => 'subject_id',
'dependent'=>'true',
'cascadeCallbacks'=>'true']);
}
}
My school.ctp :
echo $this->Form->create();
echo $this->Form->input('name');
echo $this->Form->input('exams.subject', array(
'required'=>false,
'multiple' => 'checkbox',
'options' => array(
0 => 'Tamil',
1 => 'English',
2 => 'Maths')));
echo $this->Form->button(__('Save'));
echo $this->Form->end();
In my controller:
public function school() {
$this->loadModel('Students');
$this->loadModel('Exams');
$student = $this->Students->newEntity();
if ($this->request->is('post')) {
$this->request->data['exams']['subject'] =
implode(',',$this->request->data['exams']['subject']);
$student = $this->Students->patchEntity(
$student, $this->request->data, ['associated' => ['Exams']]
);
if ($this->Students->save($student)) {
$this->Flash->success(__('The user has been saved.'));
} else {
$this->Flash->error(__('Unable to add the user.'));
}
}
}
Patching BelongsToMany Associations
You need to make sure you are able to set exams. Set accessibleFields to allow you to patch associated data
$student = $this->Students->patchEntity(
$student, $this->request->data, [
'associated' => ['Exams'],
'accessibleFields' => ['exams' => true]
]
);
You can also do this with the $_accessible property in the entity.
I've never done hasMany to belongsToMany because i don't think it works that way (I mean no harm in my words.) But I'll try to explain. Your relationships should be both belongsToMany because exams will have many students and students will have many exams. So basically they're the same either way. What you need is another table to connect them which will be called students_exams or exams_students (i think its exams_students because E comes before S) because in cake if you name everything properly most of it happens automatically.
Assuming you know how patchEntity works, creating your $this->request->data properly will patch it automatically and save it in the correct table when you save it. If you have any more questions feel free to ask more. :)
I am trying to save some data into a Joint Table using CakePHP. This is the part of the application that I would like to fix - it is a normal BelongsToMany association with additional columns:
Model > Entity:
/* Durations */
class Duration extends Entity {
protected $_accessible = [
'duration' => true,
'cost' => true,
];
}
/* Commercials */
class Commercial extends Entity {
protected $_accessible = [
'info' => true,
'commercial_durations' => true,
];
}
/* CommercialDurations */
class CommercialDuration extends Entity {
protected $_accessible = [
'duration_id' => true,
'commercial_id' => true,
'quantity' => true,
'duration' => true,
'commercial' => true,
];
}
Model > Table:
class DurationsTable extends Table {
public function initialize(array $config)
{
$this->table('durations');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsToMany('Commercials', [
'through' => 'CommercialDurations',
]);
}
}
class CommercialsTable extends Table
{
public function initialize(array $config){
$this->table('commercials');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsToMany('Durations', [
'through' => 'CommercialDurations'
]);
$this->hasMany('CommercialDurations', [
'foreignKey' => 'commercial_id'
]);
}
}
class CommercialDurationsTable extends Table {
public function initialize(array $config)
{
$this->table('commercial_durations');
$this->displayField('id');
$this->primaryKey('id');
$this->belongsTo('Durations', [
'foreignKey' => 'duration_id',
'joinType' => 'INNER'
]);
$this->belongsTo('Commercials', [
'foreignKey' => 'commercial_id',
'joinType' => 'INNER'
]);
}
}
Now, I created a new View where I want people to be able to choose one Duration, type the quantity and add that value to the database. I am using the following code:
<?php
echo $this->Form->create($commercial);
echo $this->Form->input('durations._duration', ['options' => $durations]);
echo $this->Form->input('durations._joinData.quantity');
echo $this->Form->submit(__('Next'), ['class' => 'button small right', 'escape' => false]);
echo $this->Form->end()
?>
The problem with this form is that the durations select is not showing the 'duration' field from the Durations table, but instead is showing all the fields from that table (one per row) as JSON
<option value="0">{ "id": 1, "duration": "30 sec", "cost": 450 }</option>
Once I submit the form I can't save this information into the Commercials object or CommercialDurations. This is what I get from the $this->request->data object:
[
'durations' => [
'_duration' => '2',
'_joinData' => [
'quantity' => '2'
]
]
]
The output of debug((string)$commercial) before I start the form is:
/src/Template/Commercials/features.ctp (line 22)
'{
"id": 2,
"info": "AAAAAA ",
"created": "2015-04-16T21:48:48+0000",
"updated": null,
"durations": [],
}'
How can I display the data correctly on the form?
How can I retrieve and save this data?
Thanks!!
I don't get your Models relations, since according to your post a duration belongs to a commercial and a commercial belongs to a duration.
For the matter of explaining you how the request should be sent, and how the form looks like let's assume for this example that your models are like these:
Your commercial has a commercial duration and this commercial duration belongs to a duration
So your models would look like these:
Commercial has many commercial duration.
Commercial duration belongs to commercial.
Commercial duration belongs to duration.
Duration has many commercial duration.
Your add function should be like this one (assuming you are not saving a new duration, just the commercial and the commercial duration)
$commercial = $this->Commercials->newEntity($this->request->data,[
'associated' => ['Commercialdurations']
]);
if ($this->Commercials->save($commercial)) {
$success = true;
}else{
$success = false;
}
The request data should look like this:
{
"commercialdurations":{
"Commercialduration":{
"duration_id":"1",
"quantity":"1",
"duration":"1"
}
},
"info":"something"
}
Basically you are sending the data of a new Commercial (info) and the commercial durations associated to this ( you could send multiple commercial durations).
For your form to display the duration basically you have to serialize this information un the controller, go to your add action and add this. (You could use anyway you want to retrieve the data, all that matters is that you send it back to the view)
$durations = $this->Commercials->Commercialdurations->Durations->find('list', ['limit' => 200]);
$this->set(compact('commercial',durations'));
$this->set('_serialize', ['commercial']);
Then in your form you can use the data
echo $this->Form->input('duration_id', ['options' => $durations]);
So your form would look something like this:
echo $this->Form->input('info');
echo $this->Form->input('commercials.Commercial.duration_id', ['options' => $durations]);
echo $this->Form->input('commercials.Commercial.quantity');
echo $this->Form->input('commercials.Commercial.duration');
Basically you want to send a request with all the levels of associated data.
See this other question for guidence about saving associated data:
Cake PhP 3 Saving associated data in multiples levels
To see more about how to build a form:
http://book.cakephp.org/3.0/en/views/helpers/form.html#creating-inputs-for-associated-data
I can't for the life of me figure out how to return relational data from the REST API from a custom action. For the default actions it is as easy as using the expand parameter in the request but I can't figure out how to do it for custom actions. I have tried many ways, here is the latest:
public function actionSearchbycity($city)
{
return new ActiveDataProvider([
'query' => School::find()->where(['city' => $city])->with('subjects')
]);
}
In the above example, as with all the other things I have tried this, only the School object is returned. I need the subject models as well.
Any ideas?
Try this
public function actionSearchbycity($city)
{
return new ActiveDataProvider([
'query' => School::find()->where(['city' => $city])->joinWith('subjects', false)
]);
}
If you overwrite the actions() and verbs() methods in your controller you can tell it to use a custom 'Action' model that you create.
public function actions()
{
return [
'index' => [
'class' => 'yii\rest\IndexAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'searchByCity' => [
'class' => 'app\models\actions\SearchByCityAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
'view' => [
'class' => 'yii\rest\ViewAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
],
];
}
protected function verbs()
{
return [
'index' => ['GET', 'HEAD'],
'searchByCity' => ['GET', 'HEAD'],
'view' => ['GET', 'HEAD'],
];
}
Then you can create the SearchByCityAction by extending
yii\rest\Action
It might be easier to start with a copy of yii\rest\IndexAction.
As long as you have the subjects relation setup in the School model and you've overwritten the extraFields() or fields() method to include the relation in the array response it should return that data when you call your api.
Hope this helps.
In order to get the result based on custom relation, you can do like
public function actionSearchbycity($city)
{
$q = new yii\db\Query;
$query = $q->select('school.*, subject.*')
->from('school, subject')
->where('school.id = subject.school_id');
return new ActiveDataProvider([
'query' => $query
]);
}
Here I assumed that school and subject are two tables in your db & subject table has a column named school_id which is index key whose parent is school.id.
By doing so, you can retrieve all the columns of both the tables as per the relation between them specified in where(). And it can be applied as per your custom requirement and to all tables in your db.
Hope it helps someone looking for same.