-
Notifications
You must be signed in to change notification settings - Fork 78
/
fORMRelated.php
1863 lines (1543 loc) · 72.3 KB
/
fORMRelated.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* Handles related record tasks for fActiveRecord classes
*
* The functionality of this class only works with single-field `FOREIGN KEY`
* constraints.
*
* @copyright Copyright (c) 2007-2011 Will Bond, others
* @author Will Bond [wb] <[email protected]>
* @author Will Bond, iMarc LLC [wb-imarc] <[email protected]>
* @author Jeff Turcotte <[email protected]>
* @license http://flourishlib.com/license
*
* @package Flourish
* @link http://flourishlib.com/fORMRelated
*
* @version 1.0.0b47
* @changes 1.0.0b47 Fixed issues with set_null db constraints and one-to-many table relationships [jt, 2013-05-22]
* @changes 1.0.0b46 Fixed route issue with linkRecord. [jt, 2013-04-30]
* @changes 1.0.0b45 Added the `$recursive` parameter to ::populateRecords() [wb, 2011-09-16]
* @changes 1.0.0b44 Added missing information for has and list methods to ::reflect() [wb, 2011-09-07]
* @changes 1.0.0b43 Fixed some bugs in handling relationships between PHP 5.3 namespaced classes [wb, 2011-05-26]
* @changes 1.0.0b42 Fixed a bug with ::associateRecords() not associating record set via primary key [wb, 2011-05-23]
* @changes 1.0.0b41 Fixed a bug in generating errors messages for many-to-many relationships [wb, 2011-03-07]
* @changes 1.0.0b40 Updated ::getRelatedRecordName() to use fText if loaded [wb, 2011-02-02]
* @changes 1.0.0b39 Fixed a bug with ::validate() not properly removing validation messages about a related primary key value not being present yet, if the column and related column names were different [wb, 2010-11-24]
* @changes 1.0.0b38 Updated ::overrideRelatedRecordName() to prefix any namespace from `$class` to `$related_class` if not already present [wb, 2010-11-24]
* @changes 1.0.0b37 Fixed a documentation typo [wb, 2010-11-04]
* @changes 1.0.0b36 Fixed ::getPrimaryKeys() to not throw SQL exceptions [wb, 2010-10-20]
* @changes 1.0.0b35 Backwards Compatibility Break - changed the validation messages array to use nesting for child records [wb-imarc+wb, 2010-10-03]
* @changes 1.0.0b35 Updated ::getPrimaryKeys() to always return primary keys in a consistent order when no order bys are specified [wb, 2010-07-26]
* @changes 1.0.0b34 Updated the class to work with fixes in fORMRelated [wb, 2010-07-22]
* @changes 1.0.0b33 Fixed the related table populate action to use the plural underscore_notation version of the related class name [wb, 2010-07-08]
* @changes 1.0.0b32 Backwards Compatibility Break - related table populate action now use the underscore_notation version of the class name instead of the related table name, allowing for related tables in non-standard schemas [wb, 2010-06-23]
* @changes 1.0.0b31 Fixed ::reflect() to properly show parameters for associate methods [wb, 2010-06-08]
* @changes 1.0.0b30 Fixed a bug where related record error messages could be overwritten if there were multiple related records with the same error [wb, 2010-05-29]
* @changes 1.0.0b29 Changed validation messages array to use column name keys [wb, 2010-05-26]
* @changes 1.0.0b28 Updated ::associateRecords() to accept just a single fActiveRecord [wb, 2010-05-06]
* @changes 1.0.0b27 Updated the class to force configure classes before peforming actions with them [wb, 2010-03-30]
* @changes 1.0.0b26 Fixed ::reflect() to show the proper return values for `associate`, `link` and `populate` methods [wb, 2010-03-15]
* @changes 1.0.0b25 Fixed a bug when storing a one-to-one related record with different column names on each end of the relationship [wb, 2010-03-04]
* @changes 1.0.0b24 Added the ability to associate a single record via primary key [wb, 2010-03-03]
* @changes 1.0.0b23 Fixed a column aliasing issue with SQLite [wb, 2010-01-25]
* @changes 1.0.0b22 Fixed a bug with associating a non-contiguous array of fActiveRecord objects [wb, 2009-12-17]
* @changes 1.0.0b21 Added support for the $force_cascade parameter of fActiveRecord::store(), added ::hasRecords() and fixed a bug with creating non-existent one-to-one related records [wb, 2009-12-16]
* @changes 1.0.0b20 Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
* @changes 1.0.0b19 Internal Backwards Compatibility Break - Added the `$class` parameter to ::storeManyToMany() - also fixed ::countRecords() to work across all databases, changed SQL statements to use value placeholders, identifier escaping and support schemas [wb, 2009-10-22]
* @changes 1.0.0b18 Fixed a bug in ::countRecords() that would occur when multiple routes existed to the table being counted [wb, 2009-10-05]
* @changes 1.0.0b17 Updated code for new fRecordSet API [wb, 2009-09-16]
* @changes 1.0.0b16 Fixed a bug with ::createRecord() not creating non-existent record when the related value is NULL [wb, 2009-08-25]
* @changes 1.0.0b15 Fixed a bug with ::createRecord() where foreign keys with a different column and related column name would not load properly [wb, 2009-08-17]
* @changes 1.0.0b14 Fixed a bug with ::createRecord() when a foreign key constraint is on a column other than the primary key [wb, 2009-08-10]
* @changes 1.0.0b13 ::setOrderBys() now (properly) only recognizes *-to-many relationships [wb, 2009-07-31]
* @changes 1.0.0b12 Changed how related record values are set and how related validation messages are ignored because of recursive relationships [wb, 2009-07-29]
* @changes 1.0.0b11 Fixed some bugs with one-to-one relationships [wb, 2009-07-21]
* @changes 1.0.0b10 Fixed a couple of bugs with validating related records [wb, 2009-06-26]
* @changes 1.0.0b9 Fixed a bug where ::store() would not save associations with no related records [wb, 2009-06-23]
* @changes 1.0.0b8 Changed ::associateRecords() to work for *-to-many instead of just many-to-many relationships [wb, 2009-06-17]
* @changes 1.0.0b7 Updated code for new fORM API, fixed API documentation bugs [wb, 2009-06-15]
* @changes 1.0.0b6 Updated code to use new fValidationException::formatField() method [wb, 2009-06-04]
* @changes 1.0.0b5 Added ::getPrimaryKeys() and ::setPrimaryKeys(), renamed ::setRecords() to ::setRecordSet() and ::tallyRecords() to ::setCount() [wb, 2009-06-02]
* @changes 1.0.0b4 Updated code to handle new association method for related records and new `$related_records` structure, added ::store() and ::validate() [wb, 2009-06-02]
* @changes 1.0.0b3 ::associateRecords() can now accept an array of records or primary keys instead of only an fRecordSet [wb, 2009-06-01]
* @changes 1.0.0b2 ::populateRecords() now accepts any input field keys instead of sequential ones starting from 0 [wb, 2009-05-03]
* @changes 1.0.0b The initial implementation [wb, 2007-12-30]
*/
class fORMRelated
{
// The following constants allow for nice looking callbacks to static methods
const associateRecords = 'fORMRelated::associateRecords';
const buildRecords = 'fORMRelated::buildRecords';
const countRecords = 'fORMRelated::countRecords';
const createRecord = 'fORMRelated::createRecord';
const determineRequestFilter = 'fORMRelated::determineRequestFilter';
const flagForAssociation = 'fORMRelated::flagForAssociation';
const getOrderBys = 'fORMRelated::getOrderBys';
const getRelatedRecordName = 'fORMRelated::getRelatedRecordName';
const hasRecords = 'fORMRelated::hasRecords';
const linkRecords = 'fORMRelated::linkRecords';
const overrideRelatedRecordName = 'fORMRelated::overrideRelatedRecordName';
const populateRecords = 'fORMRelated::populateRecords';
const reflect = 'fORMRelated::reflect';
const registerValidationNameMethod = 'fORMRelated::registerValidationNameMethod';
const reset = 'fORMRelated::reset';
const setOrderBys = 'fORMRelated::setOrderBys';
const setCount = 'fORMRelated::setCount';
const setPrimaryKeys = 'fORMRelated::setPrimaryKeys';
const setRecordSet = 'fORMRelated::setRecordSet';
const store = 'fORMRelated::store';
const storeManyToMany = 'fORMRelated::storeManyToMany';
const storeOneToStar = 'fORMRelated::storeOneToStar';
const validate = 'fORMRelated::validate';
/**
* A generic cache for the class
*
* @var array
*/
static private $cache = array();
/**
* Rules that control what order related data is returned in
*
* @var array
*/
static private $order_bys = array();
/**
* Names for related records
*
* @var array
*/
static private $related_record_names = array();
/**
* Methods to use for getting the name of related records when performing validation
*
* @var array
*/
static private $validation_name_methods = array();
/**
* Creates associations for one-to-one relationships
*
* @internal
*
* @param string $class The class to get the related values for
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class we are associating with the current record
* @param fActiveRecord|array|string|integer $record The record (or primary key of the record) to be associated
* @param string $route The route to use between the current class and the related class
* @return void
*/
static public function associateRecord($class, &$related_records, $related_class, $record, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
if ($record !== NULL) {
if (!$record instanceof fActiveRecord) {
$record = new $related_class($record);
}
$records = array($record);
} else {
$records = array();
}
$schema = fORMSchema::retrieve($class);
$records = fRecordSet::buildFromArray($related_class, $records);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, 'one-to-one');
self::setRecordSet($class, $related_records, $related_class, $records, $route);
self::flagForAssociation($class, $related_records, $related_class, $route);
}
/**
* Creates associations for *-to-many relationships
*
* @internal
*
* @param string $class The class to get the related values for
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class we are associating with the current record
* @param fRecordSet|array $records_to_associate An fRecordSet, an array or records, or an array of primary keys of the records to be associated
* @param string $route The route to use between the current class and the related class
* @return void
*/
static public function associateRecords($class, &$related_records, $related_class, $records_to_associate, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$primary_keys = FALSE;
if ($records_to_associate instanceof fActiveRecord) {
$records = fRecordSet::buildFromArray($related_class, array($records_to_associate));
} elseif ($records_to_associate instanceof fRecordSet) {
$records = clone $records_to_associate;
} elseif (!sizeof($records_to_associate)) {
$records = fRecordSet::buildFromArray($related_class, array());
} elseif (reset($records_to_associate) instanceof fActiveRecord) {
$records = fRecordSet::buildFromArray($related_class, $records_to_associate);
// This indicates we are working with just primary keys, so we have to call a different method
} else {
$primary_keys = TRUE;
}
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many');
if ($primary_keys) {
self::setPrimaryKeys($class, $related_records, $related_class, $records_to_associate, $route);
} else {
self::setRecordSet($class, $related_records, $related_class, $records, $route);
}
self::flagForAssociation($class, $related_records, $related_class, $route);
}
/**
* Builds a set of related records along a one-to-many or many-to-many relationship
*
* @internal
*
* @param string $class The class to get the related values for
* @param array &$values The values for the fActiveRecord class
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class that is related to the current record
* @param string $route The route to follow for the class specified
* @return fRecordSet A record set of the related records
*/
static public function buildRecords($class, &$values, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many');
// If we already have the sequence, we can stop here
if (isset($related_records[$related_table][$route]['record_set'])) {
return $related_records[$related_table][$route]['record_set'];
}
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many');
// Determine how we are going to build the sequence
if (isset($related_records[$related_table][$route]['primary_keys'])) {
$primary_key_column = current($schema->getKeys($related_table, 'primary'));
$where_conditions = array($primary_key_column . '=' => $related_records[$related_table][$route]['primary_keys']);
$order_bys = self::getOrderBys($class, $related_class, $route);
$record_set = fRecordSet::build($related_class, $where_conditions, $order_bys);
$related_records[$related_table][$route]['record_set'] = $record_set;
return $record_set;
} elseif ($values[$relationship['column']] === NULL) {
$record_set = fRecordSet::buildFromArray($related_class, array());
} else {
$column = $table . '{' . $route . '}.' . $relationship['column'];
$where_conditions = array($column . '=' => $values[$relationship['column']]);
$order_bys = self::getOrderBys($class, $related_class, $route);
$record_set = fRecordSet::build($related_class, $where_conditions, $order_bys);
}
self::setRecordSet($class, $related_records, $related_class, $record_set, $route);
return $record_set;
}
/**
* Composes text using fText if loaded
*
* @param string $message The message to compose
* @param mixed $component A string or number to insert into the message
* @param mixed ...
* @return string The composed and possible translated message
*/
static private function compose($message)
{
$args = array_slice(func_get_args(), 1);
if (class_exists('fText', FALSE)) {
return call_user_func_array(
array('fText', 'compose'),
array($message, $args)
);
} else {
return vsprintf($message, $args);
}
}
/**
* Counts the number of related one-to-many or many-to-many records
*
* @internal
*
* @param string $class The class to get the related values for
* @param array &$values The values for the fActiveRecord class
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class that is related to the current record
* @param string $route The route to follow for the class specified
* @return integer The number of related records
*/
static public function countRecords($class, &$values, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$db = fORMDatabase::retrieve($class, 'read');
$schema = fORMSchema::retrieve($class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many');
// If we already have the sequence, we can stop here
if (isset($related_records[$related_table][$route]['count'])) {
return $related_records[$related_table][$route]['count'];
}
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many');
// Determine how we are going to build the sequence
if ($values[$relationship['column']] === NULL) {
$count = 0;
} else {
$column = $relationship['column'];
$value = $values[$column];
$pk_columns = $schema->getKeys($related_table, 'primary');
// One-to-many relationships require joins
if (!isset($relationship['join_table'])) {
$table_with_route = $table . '{' . $relationship['related_column'] . '}';
$params = array("SELECT count(*) AS flourish__count FROM :from_clause WHERE ");
$params[0] .= str_replace(
'%r',
$db->escape('%r', $table_with_route . '.' . $column),
fORMDatabase::makeCondition($schema, $table, $column, '=', $value)
);
$params[] = $value;
$params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $related_table);
// Many-to-many relationships allow counting just from the join table
} else {
$params = array($db->escape(
"SELECT count(*) FROM %r WHERE %r = ",
$relationship['join_table'],
$relationship['join_column']
));
$params[0] .= $schema->getColumnInfo($table, $column, 'placeholder');
$params[] = $value;
}
$result = call_user_func_array($db->translatedQuery, $params);
$count = ($result->valid()) ? (int) $result->fetchScalar() : 0;
}
self::setCount($class, $related_records, $related_class, $count, $route);
return $count;
}
/**
* Builds the object for the related class specified
*
* @internal
*
* @param string $class The class to create the related record for
* @param array $values The values existing in the fActiveRecord class
* @param array &$related_records The related records for the record
* @param string $related_class The related class name
* @param string $route The route to the related class
* @return fActiveRecord An instance of the class specified
*/
static public function createRecord($class, $values, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$schema = fORMSchema::retrieve($class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-one');
$route = $relationship['column'];
// Determine if the relationship is one-to-one
if (isset(self::$cache['one-to-one::' . $table . '::' . $related_table . '::' . $route])) {
$one_to_one = self::$cache['one-to-one::' . $table . '::' . $related_table . '::' . $route];
} else {
$one_to_one = FALSE;
$one_to_one_relationships = fORMSchema::getRoutes($schema, $table, $related_table, 'one-to-one');
foreach ($one_to_one_relationships as $one_to_one_relationship) {
if ($relationship['column'] == $one_to_one_relationship['column']) {
$one_to_one = TRUE;
break;
}
}
self::$cache['one-to-one::' . $table . '::' . $related_table . '::' . $route] = $one_to_one;
}
// One-to-one records are stored in the related records array to support populating
if ($one_to_one) {
if (isset($related_records[$related_table][$route]['record_set'])) {
if ($related_records[$related_table][$route]['record_set']->count()) {
return $related_records[$related_table][$route]['record_set'][0];
}
return new $related_class();
}
// If the value is NULL, don't pass it to the constructor because an fNotFoundException will be thrown
if ($values[$relationship['column']] !== NULL) {
try {
$records = array(new $related_class(array($relationship['related_column'] => $values[$relationship['column']])));
} catch (fNotFoundException $e) {
$records = array();
}
} else {
$records = array();
}
$record_set = fRecordSet::buildFromArray($related_class, $records);
self::setRecordSet($class, $related_records, $related_class, $record_set, $route);
if ($record_set->count()) {
return $record_set[0];
}
return new $related_class();
}
// This allows records without a related record to return a non-existent one
if ($values[$relationship['column']] === NULL) {
return new $related_class();
}
return new $related_class(array($relationship['related_column'] => $values[$relationship['column']]));
}
/**
* Figures out the first primary key column for a related class that is not the related column
*
* @internal
*
* @param string $class The class name of the main class
* @param string $related_class The related class being filtered for
* @param string $route The route to the related class
* @return string The first primary key column in the related class
*/
static public function determineFirstPKColumn($class, $related_class, $route)
{
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$pk_columns = $schema->getKeys($related_table, 'primary');
// If there is a multi-fiend primary key we want to populate based on any field BUT the foreign key to the current class
if (sizeof($pk_columns) > 1) {
$first_pk_column = NULL;
$relationships = fORMSchema::getRoutes($schema, $related_table, $table, '*-to-one');
foreach ($pk_columns as $pk_column) {
foreach ($relationships as $relationship) {
if ($pk_column == $relationship['column']) {
continue;
}
$first_pk_column = $pk_column;
break 2;
}
}
if (!$first_pk_column) {
$first_pk_column = $pk_columns[0];
}
} else {
$first_pk_column = $pk_columns[0];
}
return $first_pk_column;
}
/**
* Figures out what filter to pass to fRequest::filter() for the specified related class
*
* @internal
*
* @param string $class The class name of the main class
* @param string $related_class The related class being filtered for
* @param string $route The route to the related class
* @return string The prefix to filter the request fields by
*/
static public function determineRequestFilter($class, $related_class, $route)
{
$table = fORM::tablize($class);
$schema = fORMSchema::retrieve($class);
$related_table = fORM::tablize($related_class);
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route);
$route_name = fORMSchema::getRouteNameFromRelationship('one-to-many', $relationship);
$primary_keys = $schema->getKeys($related_table, 'primary');
$first_pk_column = $primary_keys[0];
$filter_class = fGrammar::pluralize(fGrammar::underscorize($related_class));
$filter_class_with_route = $filter_class . '{' . $route_name . '}';
$pk_field = $filter_class . '::' . $first_pk_column;
$pk_field_with_route = $filter_class_with_route . '::' . $first_pk_column;
if (!fRequest::check($pk_field) && fRequest::check($pk_field_with_route)) {
$filter_class = $filter_class_with_route;
}
return $filter_class . '::';
}
/**
* Sets the related records for a *-to-many relationship to be associated upon fActiveRecord::store()
*
* @internal
*
* @param string $class The class to associate the related records to
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class we are associating with the current record
* @param string $route The route to use between the current class and the related class
* @return void
*/
static public function flagForAssociation($class, &$related_records, $related_class, $route=NULL)
{
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '!many-to-one');
if (!isset($related_records[$related_table][$route]['record_set']) && !isset($related_records[$related_table][$route]['primary_keys'])) {
throw new fProgrammerException(
'%1$s can only be called after %2$s or %3$s',
__CLASS__ . '::flagForAssociation()',
__CLASS__ . '::setRecordSet()',
__CLASS__ . '::setPrimaryKeys()'
);
}
$related_records[$related_table][$route]['associate'] = TRUE;
}
/**
* Gets the ordering to use when returning an fRecordSet of related objects
*
* @internal
*
* @param string $class The class to get the order bys for
* @param string $related_class The related class the ordering rules apply to
* @param string $route The route to the related table, should be a column name in the current table or a join table name
* @return array An array of the order bys - see fRecordSet::build() for format
*/
static public function getOrderBys($class, $related_class, $route)
{
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route);
if (!isset(self::$order_bys[$table][$related_table]) || !isset(self::$order_bys[$table][$related_table][$route])) {
return array();
}
return self::$order_bys[$table][$related_table][$route];
}
/**
* Gets the primary keys of the related records for *-to-many relationships
*
* @internal
*
* @param string $class The class to get the related primary keys for
* @param array &$values The values for the fActiveRecord class
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The class that is related to the current record
* @param string $route The route to follow for the class specified
* @return array The primary keys of the related records
*/
static public function getPrimaryKeys($class, &$values, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$db = fORMDatabase::retrieve($class, 'read');
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many');
if (!isset($related_records[$related_table])) {
$related_records[$related_table] = array();
}
if (!isset($related_records[$related_table][$route])) {
$related_records[$related_table][$route] = array();
}
$related_info =& $related_records[$related_table][$route];
if (!isset($related_info['primary_keys'])) {
if (isset($related_info['record_set'])) {
$related_info['primary_keys'] = $related_info['record_set']->getPrimaryKeys();
// If we don't have a record set yet we want to use a single SQL query to just get the primary keys
} else {
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many');
$related_pk_columns = $schema->getKeys($related_table, 'primary');
$column_info = $schema->getColumnInfo($related_table);
$column = $relationship['column'];
$aliased_related_pk_columns = array();
foreach ($related_pk_columns as $related_pk_column) {
// We explicitly alias the columns due to SQLite issues
$aliased_related_pk_columns[] = $db->escape("%r AS %r", $related_table . '.' . $related_pk_column, $related_pk_column);
}
if (isset($relationship['join_table'])) {
$table_with_route = $table . '{' . $relationship['join_table'] . '}';
} else {
$table_with_route = $table . '{' . $relationship['related_column'] . '}';
}
$column = $relationship['column'];
$related_column = $relationship['related_column'];
$params = array(
$db->escape(
sprintf(
"SELECT %s FROM :from_clause WHERE",
join(', ', $aliased_related_pk_columns)
) . " %r = ",
$table_with_route . '.' . $column
),
);
$params[0] .= $schema->getColumnInfo($table, $column, 'placeholder');
$params[] = $values[$column];
$params[0] .= " :group_by_clause ";
if (!$order_bys = self::getOrderBys($class, $related_class, $route)) {
$order_bys = array();
foreach ($related_pk_columns as $related_pk_column) {
$order_bys[$related_pk_column] = 'ASC';
}
}
$params[0] .= " ORDER BY ";
$params = fORMDatabase::addOrderByClause($db, $schema, $params, $related_table, $order_bys);
$params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $related_table);
$result = call_user_func_array($db->translatedQuery, $params);
$primary_keys = array();
foreach ($result as $row) {
if (sizeof($row) > 1) {
$primary_key = array();
foreach ($row as $column => $value) {
$value = $db->unescape($column_info[$column]['type'], $value);
$primary_key[$column] = $value;
}
$primary_keys[] = $primary_key;
} else {
$column = key($row);
$primary_keys[] = $db->unescape($column_info[$column]['type'], $row[$column]);
}
}
$related_info['record_set'] = NULL;
$related_info['count'] = sizeof($primary_keys);
$related_info['associate'] = FALSE;
$related_info['primary_keys'] = $primary_keys;
}
}
return $related_info['primary_keys'];
}
/**
* Returns the record name for a related class
*
* The default record name of a related class is the result of
* fGrammar::humanize() called on the class.
*
* @internal
*
* @param string $class The class to get the related class name for
* @param string $related_class The related class to get the record name of
* @return string The record name for the related class specified
*/
static public function getRelatedRecordName($class, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route);
if (!isset(self::$related_record_names[$table]) ||
!isset(self::$related_record_names[$table][$related_class]) ||
!isset(self::$related_record_names[$table][$related_class][$route])) {
return fORM::getRecordName($related_class);
}
// If fText is loaded, use it
if (class_exists('fText', FALSE)) {
return call_user_func(
array('fText', 'compose'),
str_replace('%', '%%', self::$related_record_names[$table][$related_class][$route])
);
}
return self::$related_record_names[$table][$related_class][$route];
}
/**
* Indicates if a record has a one-to-one or any *-to-many related records
*
* @internal
*
* @param string $class The class to check related records for
* @param array &$values The values for the record we are checking
* @param array &$related_records The related records for the record we are checking
* @param string $related_class The related class we are checking for
* @param string $route The route to the related class
* @return void
*/
static public function hasRecords($class, &$values, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '!many-to-one');
if (!isset($related_records[$related_table][$route]['count'])) {
if (fORMSchema::isOneToOne($schema, $table, $related_table, $route)) {
self::createRecord($class, $values, $related_records, $related_class, $route);
} else {
self::countRecords($class, $values, $related_records, $related_class, $route);
}
}
return (boolean) $related_records[$related_table][$route]['count'];
}
/**
* Parses associations for many-to-many relationships from the page request
*
* @internal
*
* @param string $class The class to get link the related records to
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The related class to populate
* @param string $route The route to the related class
* @return void
*/
static public function linkRecords($class, &$related_records, $related_class, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route_name = fORMSchema::getRouteName($schema, $table, $related_table, $route, 'many-to-many');
$relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, 'many-to-many');
$field_table = $relationship['related_table'];
$field_column = '::' . $relationship['related_column'];
$field = $field_table . $field_column;
$field_with_route = $field_table . '{' . $route_name . '}' . $field_column;
// If there is only one route and they specified the route instead of leaving it off, use that
if ($route !== NULL || $route === NULL && !fRequest::check($field) && fRequest::check($field_with_route)) {
$field = $field_with_route;
}
$record_set = fRecordSet::build(
$related_class,
array(
$relationship['related_column'] . '=' => fRequest::get($field, 'array', array())
)
);
self::associateRecords($class, $related_records, $related_class, $record_set, $route_name);
}
/**
* Does an [http://php.net/array_diff array_diff()] for two arrays that have arrays as values
*
* @param array $array1 The array to remove items from
* @param array $array2 The array of items to remove
* @return array The items in `$array1` that were not also in `$array2`
*/
static private function multidimensionArrayDiff($array1, $array2)
{
$output = array();
foreach ($array1 as $sub_array1) {
$remove = FALSE;
foreach ($array2 as $sub_array2) {
if ($sub_array1 == $sub_array2) {
$remove = TRUE;
}
}
if (!$remove) {
$output[] = $sub_array1;
}
}
return $output;
}
/**
* Allows overriding of default record names or related records
*
* The default record name of a related record is the result of
* fGrammar::humanize() called on the class name.
*
* @param mixed $class The class name or instance of the class to set the related record name for
* @param mixed $related_class The name of the related class, or an instance of it
* @param string $record_name The human version of the related record
* @param string $route The route to the related class
* @return void
*/
static public function overrideRelatedRecordName($class, $related_class, $record_name, $route=NULL)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$class = fORM::getClass($class);
$table = fORM::tablize($class);
$related_class = fORM::getClass($related_class);
$related_class = fORM::getRelatedClass($class, $related_class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many');
if (!isset(self::$related_record_names[$table])) {
self::$related_record_names[$table] = array();
}
if (!isset(self::$related_record_names[$table][$related_class])) {
self::$related_record_names[$table][$related_class] = array();
}
self::$related_record_names[$table][$related_class][$route] = $record_name;
}
/**
* Sets the values for records in a one-to-many relationship with this record
*
* @internal
*
* @param string $class The class to populate the related records of
* @param array &$related_records The related records existing for the fActiveRecord class
* @param string $related_class The related class to populate
* @param string $route The route to the related class
* @param boolean $recursive If a recursive populate should be performed on the child records
* @return void
*/
static public function populateRecords($class, &$related_records, $related_class, $route=NULL, $recursive=FALSE)
{
fActiveRecord::validateClass($related_class);
fActiveRecord::forceConfigure($related_class);
$table = fORM::tablize($class);
$related_table = fORM::tablize($related_class);
$schema = fORMSchema::retrieve($class);
$pk_columns = $schema->getKeys($related_table, 'primary');
$first_pk_column = self::determineFirstPKColumn($class, $related_class, $route);
$filter = self::determineRequestFilter($class, $related_class, $route);
$pk_field = $filter . $first_pk_column;
$input_keys = array_keys(fRequest::get($pk_field, 'array', array()));
$records = array();
foreach ($input_keys as $input_key) {
fRequest::filter($filter, $input_key);
// Try to load the value from the database first
try {
if (sizeof($pk_columns) == 1) {
$primary_key_values = fRequest::get($first_pk_column);
} else {
$primary_key_values = array();
foreach ($pk_columns as $pk_column) {
$primary_key_values[$pk_column] = fRequest::get($pk_column);
}
}
$record = new $related_class($primary_key_values);
} catch (fNotFoundException $e) {
$record = new $related_class();
}
$record->populate($recursive);
$records[] = $record;
fRequest::unfilter();
}
$record_set = fRecordSet::buildFromArray($related_class, $records);
self::setRecordSet($class, $related_records, $related_class, $record_set, $route);
self::flagForAssociation($class, $related_records, $related_class, $route);
}
/**
* Adds information about methods provided by this class to fActiveRecord
*
* @internal
*
* @param string $class The class to reflect the related record methods for
* @param array &$signatures The associative array of `{method_name} => {signature}`
* @param boolean $include_doc_comments If the doc block comments for each method should be included
* @return void
*/
static public function reflect($class, &$signatures, $include_doc_comments)
{
$table = fORM::tablize($class);
$schema = fORMSchema::retrieve($class);
$one_to_one_relationships = $schema->getRelationships($table, 'one-to-one');
$one_to_many_relationships = $schema->getRelationships($table, 'one-to-many');
$many_to_one_relationships = $schema->getRelationships($table, 'many-to-one');
$many_to_many_relationships = $schema->getRelationships($table, 'many-to-many');
$to_one_relationships = array_merge($one_to_one_relationships, $many_to_one_relationships);
$to_many_relationships = array_merge($one_to_many_relationships, $many_to_many_relationships);
$to_one_created = array();
foreach ($to_one_relationships as $relationship) {
$related_class = fORM::classize($relationship['related_table']);
$related_class = fORM::getRelatedClass($class, $related_class);
if (isset($to_one_created[$related_class])) {
continue;
}
$routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], '*-to-one');
$route_names = array();
foreach ($routes as $route) {
$route_names[] = fORMSchema::getRouteNameFromRelationship('*-to-one', $route);
}
$signature = '';
if ($include_doc_comments) {
$signature .= "/**\n";
$signature .= " * Creates the related " . $related_class . "\n";
$signature .= " * \n";
if (sizeof($route_names) > 1) {
$signature .= " * @param string \$route The route to the related class. Must be one of: '" . join("', '", $route_names) . "'.\n";
}
$signature .= " * @return " . $related_class . " The related object\n";
$signature .= " */\n";