7
7
use Cake \Http \Exception \BadRequestException ;
8
8
use Cake \ORM \Association ;
9
9
use Cake \ORM \Table ;
10
+ use Cake \ORM \TableRegistry ;
10
11
use Cake \Utility \Hash ;
11
12
use Cake \Utility \Inflector ;
12
13
use CrudJsonApi \Listener \JsonApi \DocumentValidator ;
@@ -169,7 +170,9 @@ public function afterFind($event)
169
170
}
170
171
171
172
/**
172
- * beforeSave() event.
173
+ * beforeSave() event used to prevent users from sending `hasMany` relationships when POSTing and
174
+ * to prevent them from sending `hasMany` relationships not belonging to this primary resource
175
+ * when PATCHing.
173
176
*
174
177
* @param \Cake\Event\Event $event Event
175
178
* @return void
@@ -179,15 +182,54 @@ public function beforeSave($event)
179
182
{
180
183
// generate a flat list of hasMany relationships for the current model
181
184
$ entity = $ event ->getSubject ()->entity ;
182
- $ hasManyAssociations = $ this ->_getAssociationsList ($ entity , [Association::ONE_TO_MANY ]); // hasMany
185
+ $ hasManyAssociations = $ this ->_getAssociationsList ($ entity , [Association::ONE_TO_MANY ]);
183
186
184
- // stop propagation if hasMany relationship(s) are detected in the request data
185
- // and thus the client is trying to side-post/create related records
187
+ if (empty ($ hasManyAssociations )) {
188
+ return ;
189
+ }
190
+
191
+ // must be PATCH so verify hasMany relationships before saving
186
192
foreach ($ hasManyAssociations as $ associationName ) {
187
193
$ key = Inflector::tableize ($ associationName );
188
- if (isset ($ entity ->$ key )) {
189
- throw new BadRequestException ("JSON API 1.0 does not support side-posting (hasMany relationship data detected in the request body) " );
194
+
195
+ // do nothing if association is not hasMany
196
+ if (!isset ($ entity ->$ key )) {
197
+ continue ;
198
+ }
199
+
200
+ // prevent clients attempting to side-post/create related hasMany records
201
+ if ($ this ->_request ()->getMethod () === 'POST ' ) {
202
+ throw new BadRequestException ("JSON API 1.0 does not support sideposting (hasMany relationships detected in the request body) " );
203
+ }
204
+
205
+ // hasMany found in the entity, extract ids from the request data
206
+ $ primaryResourceId = $ this ->_controller ()->request ->getData ('id ' );
207
+
208
+ /** @var array $hasManyIds */
209
+ $ hasManyIds = Hash::extract ($ this ->_controller ()->request ->getData ($ key ), '{n}.id ' );
210
+ $ hasManyTable = TableRegistry::get ($ associationName );
211
+
212
+ // query database only for hasMany that match both passed id and the id of the primary resource
213
+ /** @var string $entityForeignKey */
214
+ $ entityForeignKey = $ hasManyTable ->getAssociation ($ entity ->getSource ())->getForeignKey ();
215
+ $ query = $ hasManyTable ->find ()
216
+ ->select (['id ' ])
217
+ ->where ([
218
+ $ entityForeignKey => $ primaryResourceId ,
219
+ 'id IN ' => $ hasManyIds ,
220
+ ]);
221
+
222
+ // throw an exception if number of database records does not exactly matches passed ids
223
+ if (count ($ hasManyIds ) !== $ query ->count ()) {
224
+ throw new BadRequestException ("One or more of the provided relationship ids for $ associationName do not exist in the database " );
190
225
}
226
+
227
+ // all good, replace entity data with fetched entities before saving
228
+ $ entity ->$ key = $ query ->toArray ();
229
+
230
+ // lastly, set the `saveStrategy` for this hasMany to `replace` so non-matching existing records will be removed
231
+ $ repository = $ event ->getSubject ()->query ->getRepository ();
232
+ $ repository ->getAssociation ($ associationName )->setSaveStrategy ('replace ' );
191
233
}
192
234
}
193
235
@@ -540,11 +582,7 @@ protected function _sortParameter($sortFields, Subject $subject, $options)
540
582
}
541
583
542
584
/**
543
- * Adds belongsTo data to the find() result so the 201 success response
544
- * is able to render the jsonapi `relationships` member.
545
- *
546
- * Please note that we are deliberately NOT creating a new find query as
547
- * this would not respect non-accessible fields.
585
+ * Adds belongsTo data to the find() result.
548
586
*
549
587
* @param \Cake\Event\Event $event Event
550
588
* @return void
@@ -556,38 +594,38 @@ protected function _insertBelongsToDataIntoEventFindResult($event)
556
594
$ associations = $ repository ->associations ();
557
595
558
596
foreach ($ associations as $ association ) {
559
- $ type = $ association ->type ();
560
-
561
- // handle `belongsTo` and `hasOne` relationships
562
- if ($ type === Association::MANY_TO_ONE || $ type === Association::ONE_TO_ONE ) {
563
- $ associationTable = $ association ->getTarget ();
564
- $ foreignKey = $ association ->getForeignKey ();
565
-
566
- $ result = $ associationTable
567
- ->find ()
568
- ->select (['id ' ])
569
- ->where ([$ association ->getName () . '.id ' => $ entity ->$ foreignKey ])
570
- ->first ();
571
-
572
- // Unfortunately, _propertyName is protected. We have got serious reason to use it though.
573
- $ reflectedAssoc = new \ReflectionClass ('Cake\ORM\Association ' );
574
- $ propertyNameProp = $ reflectedAssoc ->getProperty ('_propertyName ' );
575
- $ propertyNameProp ->setAccessible (true );
576
- $ key = $ propertyNameProp ->getValue ($ association );
577
-
578
- // There are cases when _propertyName is not set and we go default then
579
- if (!$ key ) {
580
- $ key = Inflector::tableize ($ association ->getName ());
581
- $ key = Inflector::singularize ($ key );
582
- }
583
-
584
- $ entity ->$ key = $ result ;
597
+ $ associationType = $ association ->type ();
598
+ $ associationTable = $ association ->getTarget (); // Users
599
+
600
+ // belongsTo and HasOne
601
+ if ($ associationType === Association::MANY_TO_ONE || $ associationType === Association::ONE_TO_ONE ) {
602
+ $ foreignKey = $ association ->getForeignKey (); // user_id
603
+ $ associationId = $ entity ->$ foreignKey ; // 1234
604
+
605
+ if (!empty ($ associationId )) {
606
+ $ associatedEntity = $ associationTable ->newEntity ();
607
+ $ associatedEntity ->set ('id ' , $ associationId );
608
+
609
+ // generate key name required for neoMerx to find and use the entity data
610
+ // => ?!? => unfortunately, _propertyName is protected. We have got serious reason to use it though
611
+ $ reflectedAssoc = new \ReflectionClass ('Cake\ORM\Association ' );
612
+ $ propertyNameProp = $ reflectedAssoc ->getProperty ('_propertyName ' );
613
+ $ propertyNameProp ->setAccessible (true );
614
+ $ key = $ propertyNameProp ->getValue ($ association );
615
+
616
+ if (!$ key ) {
617
+ $ key = Inflector::singularize ($ association ->getName ()); // Users
618
+ $ key = Inflector::underscore ($ key ); // user
619
+ }
585
620
586
- //Also insert the contained associations into the query
587
- if (isset ($ event ->getSubject ()->query )) {
588
- $ event ->getSubject ()->query ->contain ($ association ->getName ());
621
+ $ entity ->set ($ key , $ associatedEntity );
589
622
}
590
623
}
624
+
625
+ // insert the contained associations into the query
626
+ if (!empty ($ event ->getSubject ()->query )) {
627
+ $ event ->getSubject ()->query ->contain ($ association ->getName ());
628
+ }
591
629
}
592
630
593
631
$ event ->getSubject ()->entity = $ entity ;
0 commit comments