-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathModel.php
1455 lines (1258 loc) · 40.4 KB
/
Model.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
/**
* @file Model.php
* @author Gabriele Tozzi <[email protected]>
* @package DoPhp
* @brief Class used to represent a DoPhp Model and related classes
* @deprecated Classes in this file where work in progress, some functions are
* incomplete and backward compatibility could be broken without notice
* in future versions. It will not be completed anytime soon and may even
* be removed.
* The new backend, form and field classes are replacing it.
*/
namespace dophp;
/**
* Represents a DoPhp Model.
* A model extends a database table handling the conversion of the data from
* machine to human-friendly format. It takes care about labels, number and date
* formatting, defining "virtual" columns, etc...
*
* @deprecated See the new backend, form and field classes
*/
abstract class Model {
/** Database instance */
protected $_db;
/**
* The name of the underlying database table, must be overridden.
* Will be replaced with a real instance of the table by constructor.
*/
protected $_table = null;
/**
* Field description array, should be overriden in sub-class
* defining an associative array or arrays in the format accepted by
* FieldDefinition::__construct()
*
* Array keys are used as \<name\> attribute when not numeric if a name has not
* already been provided inside the field definition. Numeric keys are ignored.
*
* @see initFields()
*/
protected $_fields = null;
/**
* Hunam-readable names for table items, singular and plural forms
* (es. ['user', 'users']), should be overriden in sub-class
* @see initNames()
*/
protected $_names = null;
/**
* The base filter to apply on the table (by exaple, for access limiting),
* associative array of field => \<value(s)\>. If value is an array, multiple
* values are allowed with and OR condition.
* initFilter() also supports advanced filters.
* @see initFilter()
* @see SimpleAccessFilter::__construct()
*/
protected $_filter = null;
/**
* The default order to apply when selecting (if any)
*/
protected $_order = null;
/**
* Used internally for caching summary cols
*/
private $__sumColsCache = [];
/**
* Used internally for caching relations
*/
private $__relsCache = [];
/**
* Class constuctor
*
* @param $db object: Database instance
*/
public function __construct($db) {
$this->_db = $db;
if( $this->_fields === null )
$this->_fields = $this->initFields();
if( $this->_names === null )
$this->_names = $this->initNames();
if( $this->_filter === null )
$this->_filter = $this->initFilter();
if( $this->_order === null )
$this->_order = $this->initOrder();
$this->_table = new Table($this->_db, $this->_table);
// Build and validate the filter
if( ! $this->_filter )
$this->_filter = new NullAccessFilter();
elseif( gettype($this->_filter) == 'object' ) {
if( ! $this->_filter instanceof AccessFilterInterface )
throw new \InvalidArgumentException('Invalid filter class');
} elseif( ! is_array($this->_filter) )
throw new \UnexpectedValueException('Invalid filter format');
else
$this->_filter = new SimpleAccessFilter($this->_filter);
// Clean and validate the fields array
if( ! $this->_fields || ! is_array($this->_fields) )
throw new \InvalidArgumentException('Invalid fields');
foreach( $this->_fields as $f => & $d )
if( ! $d instanceof FieldDefinition )
if( is_array($d) ) {
if( ! isset($d['name']) && ! is_int($f) )
$d['name'] = $f;
$d = new FieldDefinition($d);
} else {
throw new \InvalidArgumentException('Every field must be array or FieldDefinition');
}
unset($d);
}
/**
* Returns fields for this table, called when $this->_fields is not defined.
* This way of loading fields allows the usage of gettext.
*
* @return array in $_fields valid format
* @see $_fields
*/
protected function initFields() {
throw new \dophp\NotImplementedException('Unimplemented');
}
/**
* Returns names for this table's items, called when $this->_names is not
* defined.
* This way of loading names allows the usage of gettext.
*
* @return array in $_names valid format
* @see $_names
*/
protected function initNames() {
throw new \dophp\NotImplementedException('Unimplemented');
}
/**
* Returns filter to apply to this table's content, called when $this->_filter
* is not defined.
* Allows filter definition at runtime
*
* @return array in $_filter valid format OR AccessFilterInterface instance
* @see $_filter
* @see SimpleAccessFilter::__construct()
*/
protected function initFilter() {
return array();
}
/**
* Returns default order to apply when selecting, called when $this->_order
* is not defined.
* Allows order definition at runtime
*
* @return array in $_order valid order
* @see $_order
*/
protected function initOrder() {
return array();
}
/**
* Returns singular and plural names for the elements of this model
*
* @return array [\<singular\>, \<plural\>]
*/
public function getNames() {
return $this->_names;
}
/**
* Returns all column labels
*
* @return array Associative array of column [ \<name\> =\> \<label\> ]
*/
public function getLabels() {
$labels = array();
foreach( $this->_fields as $k => $f )
if( $f->label )
$labels[$k] = $f->label;
return $labels;
}
/**
* Returns validation rules
*
* @param $mode string: The action to get the rules for (insert or update).
* If unvalid or null acton is given, only rules common to all
* actions are returned
* @return array Associative array [ \<name\> => [\<type\>, [\<rules\>]] ]
*/
public function getRules($mode=null) {
$rules = array();
foreach( $this->_fields as $k => $f )
if( isset($f->dtype) ) {
if( $mode=='edit' && ! $f->edit )
continue; // Skip rules for non-editable fields on edit mode
if( $f->i18n ) {
// Copy main rule to all childs
$sub = array();
foreach( \DoPhp::lang()->getSupportedLanguages() as $l )
$sub[$l] = array($f->dtype, $f->dopts);
$rules[$k] = array('array', array('rules'=>$sub));
continue;
}
$rules[$k] = array($f->dtype, $f->dopts);
}
// Parse "required" fields according to mode
foreach( $rules as & $r ) {
if( ! isset($r[1]['required']) )
continue;
if( gettype($r[1]['required']) == 'boolean' )
continue;
$r[1]['required'] = ($r[1]['required']==$mode);
}
unset($r);
return $rules;
}
/**
* Returns the data for rendering an insert form and runs the insert query
* when data is submitted
*
* @see _insertOrEdit()
*/
public function insert(& $post, & $files) {
return $this->_insertOrEdit(null, $post, $files);
}
/**
* Returns the data for rendering an edit form and runs the update query
* when data is submitted
*
* @see _insertOrEdit()
*/
public function edit($pk, & $post, & $files) {
return $this->_insertOrEdit($pk, $post, $files);
}
/**
* Returns the data for rendering an insert or edit form and runs the
* insert or update query when data is submitted
*
* @param $pk mixed: The PK to select the record to be edited, null on insert
* @param $post array: Associative array of data, usually $_POST
* @param $post array: Associative array of file data, usually $_FILES
* @return array Associative array of column [ \<name\> => \<label\> ] or
* null on success
*/
protected function _insertOrEdit($pk, & $post, & $files) {
if( $pk === null )
$mode = 'insert';
else
$mode = 'edit';
// Check if data has been submitted
$data = null;
$errors = null;
if( $post ) {
foreach( $this->_fields as $k => $f ) {
// Set static values
if( isset($f->value) )
$post[$k] = $f->value;
// Remove non editable fields on edit mode
if( $mode=='edit' && ! $f->edit && array_key_exists($k, $post) )
unset($post[$k]);
}
// Data has been submitted
list($data,$errors) = $this->validate($post, $files, $mode, $pk);
$related = array();
if( ! $errors ) {
if( ! $this->isAllowed($data, true) )
throw new \RuntimeException('Saving forbidden data');
foreach( $this->_fields as $k => $f ) {
// Do not update empty password and file fields
if( $mode == 'edit' && in_array($f->rtype,array('password','file')) && array_key_exists($k,$data) && ! $data[$k] )
unset($data[$k]);
// Runs postprocessors
if( isset($f->postp) && array_key_exists($k,$data) ) {
$postp = $f->postp;
$data[$k] = $postp($data[$k]);
}
// Save files
if( $f->rtype == 'file' && isset($data[$k]) )
$data[$k] = $this->_saveFile($name, $data[$k]);
// Handle multi fields
if( $f->dtype == 'array' )
if( ! $f->nmtab )
$data[$k] = serialize($data[$k]);
else { // Move data into "related" array and handle it later
$related[$k] = $data[$k];
unset($data[$k]);
}
}
// Start tansaction
$this->_db->beginTransaction();
// Data is good, write the update
if( $mode == 'edit' ) {
$this->_beforeEdit($pk, $data, $related);
foreach( $data as $k => $v )
if( isset($this->_fields[$k]) && $this->_fields[$k]->i18n ) {
// Leave text ID untouched and update text instead
$txtid = $this->_table->get($pk, [$k])[$k];
\DoPhp::lang()->updText($txtid, $v);
unset($data[$k]);
}
if( count($data) )
$this->_table->update($pk, $data);
} elseif( $mode == 'insert' ) {
$this->_beforeInsert($data, $related);
foreach( $data as $k => & $v )
if( $this->_fields[$k]->i18n ) {
// Insert text into text table and replace l18n field
// with its text ID
$txtid = \DoPhp::lang()->newText($v);
$v = $txtid;
}
unset($v);
$pk = $this->_table->insert($data);
} else
throw new \LogicException('This should never happen');
// Update related data, if needed
foreach( $related as $k => $v ) {
if( $v === null )
continue;
$rinfo = $this->__analyzeRelation($this->_fields[$k]);
// Normalize $v syntax: $v is now an array of associative arrays: $pk => [ extra fields ]
// Convert fields using the old syntax (array of pks) to the new one
$new = array();
foreach( $v as $kk => $vv )
if( ! is_array($vv) )
$new[$vv] = array();
else
$new[$kk] = $vv;
$v = $new;
// Delete unwanted relations and update all relations already present
// that could have different data
foreach( $rinfo['nm']->select(array($rinfo['ncol'] => $pk), true) as $x => $r ) {
if( ! $v || ! array_key_exists($r[$rinfo['mcol']], $v) )
$rinfo['nm']->delete($r);
elseif( array_key_exists($r[$rinfo['mcol']], $v) && is_array($v[$r[$rinfo['mcol']]]) ) {
if( count($v[$r[$rinfo['mcol']]]) ) {
$pkdata = array($rinfo['ncol'] => $pk, $rinfo['mcol'] => $r[$rinfo['mcol']]);
$rinfo['nm']->update($pkdata, $v[$r[$rinfo['mcol']]]);
}
}
}
// Insert missing relations
if( $v )
foreach( $v as $kk => $vv ) {
$pkdata = array($rinfo['ncol'] => $pk, $rinfo['mcol'] => $kk);
$existing = false;
foreach( $rinfo['nm']->select($pkdata,true,true) as $r ) {
$existing = true;
break;
}
if( ! $existing )
$rinfo['nm']->insert(array_merge($pkdata, $vv));
}
}
// Run after insert/edit methods
switch($mode) {
case 'insert':
$this->_afterInsert($pk, $data, $related);
break;
case 'edit':
$this->_afterEdit($pk, $data, $related);
break;
}
// Commit
$this->_db->commit();
return null;
}
}
// Retrieve hard data from the DB
if( $mode == 'edit' ) {
$record = $this->_table->get($pk);
foreach( $this->_fields as $n => $f )
if( $f->dtype == 'array' )
if( ! $f->nmtab )
$record[$n] = unserialize($record[$n]);
else { // Read data from relation
$rinfo = $this->__analyzeRelation($f);
$record[$n] = array();
foreach( $rinfo['nm']->select(array($rinfo['ncol']=>$pk), array($rinfo['mcol'])) as $r )
$record[$n][] = $r[$rinfo['mcol']];
}
if( ! $this->isAllowed($record, true) )
throw new \RuntimeException('Loading forbidden data');
}
// Build fields array
$fields = array();
foreach( $this->_fields as $k => $f ) {
if( ! $f->rtype )
continue;
if( $mode=='edit' && ! $f->edit ) // Don't render non editable fields in edit form
continue;
if( $f->i18n ) {
// Explode l18n field
foreach( \DoPhp::lang()->getSupportedLanguages() as $l ) {
$fl = $f;
$fl->label = $this->__buildLangLabel($fl->label, $l);
$val = $data&&isset($data[$k][$l]) ? $data[$k][$l] : (isset($record)?\DoPhp::lang()->getText($record[$k],$l):$f->getDefault());
$err = $errors&&isset($errors[$k][$l]) ? $errors[$k][$l] : null;
$fields["{$k}[{$l}]"] = $this->__buildField($k, $fl, $val, $err);
}
} else {
$val = $data&&isset($data[$k]) ? $data[$k] : (isset($record)?$record[$k]:$f->getDefault());
$err = $errors&&isset($errors[$k]) ? $errors[$k] : null;
$fields[$k] = $this->__buildField($k, $f, $val, $err);
}
}
return $fields;
}
/**
* Returns the data for rendering a display page
*
* @param $pk mixed: The PK to select the record to be read
* @return Array of Field instances
*/
public function read($pk) {
if( ! $pk )
throw new \UnexpectedValueException('Unvalid or missing pk');
list($data, $count) = $this->__readData('view', new Where($this->_table->parsePkArgs($pk)));
if( ! $data )
throw new \RuntimeException('Loading forbidden data');
if( count($data) != 1 )
throw new \RuntimeException('Received too many data rows: ' . count($data) . '/' . $count);
return array_shift($data);
}
/**
* Returns the data for rendering a summary table
*
* @todo Will supporto filtering, ordering, etc...
* @return Array of data: associative array of data as \<pk\> => \<Field\>
* count: total number of records found
* heads: column headers
*/
public function table() {
return $this->__readData('admin');
}
/**
* Returns the data for rendering a summary table or a view page
*
* @todo Support filtering, ordering, etc... (datatables server-side)
* @param $action string: The action name (admin|view)
* @param $pk Where instance: The PK to use for view action
* @return Array of data: associative array of data as \<pk\> => \<Field\>
* count: total number of records found
* heads: column headers
*/
private function __readData($action, Where $pk=null) {
if( $action != 'admin' && $action != 'view' )
throw new \UnexpectedValueException("Invalid action $action");
if( $action == 'view' && ! $pk )
throw new \UnexpectedValueException("Must provide a PK for view action");
// Init variables
$cols = array();
$refs = array();
$joins = array();
$labels = array();
$allLabels = $this->getLabels();
// Add main columns from field definitions
foreach( $this->_fields as $k => $f )
if( ( ($action=='admin' && $f->rtab) || ($action=='view' && $f->rview) ) ) {
if( $f->name && ! $f->nmtab )
$cols[] = $k;
$labels[$k] = $allLabels[$k];
if( isset($f->ropts['refer']) && ! $f->nmtab ) {
$refmod = \DoPhp::model($f->ropts['refer']['model']);
if( ! in_array($refmod, $refs) )
$refs[] = $refmod;
}
}
// Add component columns from field definitions, if missing
foreach( $this->_fields as $f )
if( isset($f->ropts['comp']) )
foreach( $f->ropts['comp'] as $col )
if( ! in_array($col, $cols) )
$cols[] = $col;
// Add PK columns, if missing (required to itenfity the row)
foreach( $this->_table->getPk() as $c )
if( ! in_array($c, $cols) )
$cols[] = $c;
// Build joins
foreach( $refs as $r )
$joins[] = new Join($r->getTable(), $r->summaryCols());
// Prepare filter
if( $action == 'view' ) {
$filter = $pk;
$filter->add($this->_filter->getRead());
} else
$filter = $this->_filter->getRead();
// Run the query and process data
$data = array();
foreach( $this->_table->select($filter, $cols, null, $joins, $this->_order) as $x => $res ) {
$row = array();
foreach( $this->_fields as $k => $f )
if( ( ($action=='admin' && $f->rtab) || ($action=='view' && $f->rview) ) )
$row[$k] = isset($f->ropts['func']) ? new RenderedField($res,$f) : new Field($res[$k],$f,$res);
$data[$this->formatPk($res)] = $row;
}
$count = $this->_db->foundRows();
return array($data, $count, $labels);
}
/**
* Try to delete an element, returns a human.friendly error when failed
*
* @param $pk mixed: The PK to select the record to be read
* @return array: [User errror message, Detailed error message] or NULL on success
*/
public function delete($pk) {
if( ! $pk )
throw new \UnexpectedValueException('Unvalid or missing pk');
$this->_db->beginTransaction();
$this->_beforeDelete($pk);
try {
$this->_table->delete($pk);
} catch( \PDOException $e ) {
$this->_db->rollBack();
list($scode, $mcode, $mex) = $e->errorInfo;
if( $scode == '23000' && $mcode == 1451 )
return array(_('item is in use'), $e->getMessage());
else
return array($e->getMessage(), $e->getMessage());
}
$this->_afterDelete($pk);
$this->_db->commit();
return null;
}
/**
* Builds related data for the given field as an associative array of key => FormFieldData
*
* @param $name string: The name of the field
* @param $q string: The search query string to filter (null = don't filter)
* @param $pks mixed: If given, only return the given PK or PKs if array
*
* @return array: associative array of key => FormFieldData
*/
public function fieldData($name, $q=null, $pks=null) {
if( $pks !== null && ! is_array($pks) )
$pks = array($pks);
$data = null;
$f = & $this->_fields[$name];
if( $f->rtype == 'select' || $f->rtype == 'multi' || $f->rtype == 'auto' ) {
// Retrieve data
$groups = array();
if( array_key_exists('data',$f->ropts) )
$data = $f->ropts['data'];
else {
if( ! isset($f->ropts['refer']) )
throw New \UnexpectedValueException("Need refer or data for $k field");
$rmodel = \DoPhp::model($f->ropts['refer']['model']);
$data = $rmodel->summary($f->ropts['refer']['filter'], $f->ropts['refer']['summary']);
if( isset($f->ropts['group']) )
foreach( $data as $pk => $v )
$groups[$pk] = $rmodel->read($pk)[$f->ropts['group']]->format();
}
// Filter data, apply query and PKs
if( isset($this->_filter) || ($q !== null && $q !== '') || $pks )
foreach( $data as $pk => $v )
if(
( isset($this->_filter) && ! $this->_filter->isAllowed($pk, $v, false) )
||
( $q !== null && $q !== '' && strpos(strtolower($v), $q) === false )
||
( $pks && ! in_array($pk, $pks) )
)
unset($data[$pk]);
// Assemble data
foreach( $data as $k => & $v )
$v = new FormFieldData($k, $v, isset($groups[$k])?$groups[$k]:null);
unset($v);
}
return $data;
}
/**
* Builds a localized label
*/
private function __buildLangLabel($label, $lang) {
return $label . ' (' . \Locale::getDisplayLanguage($lang) . ')';
}
/**
* Builds a single field, internal function
*
* @param $k string: The field name
* @param $f array: The field definition
* @param $value mixed: The field value
* @param $error string: The error message
*
* @return FormField: The built field
*/
private function __buildField($k, & $f, $value, $error) {
if( ! ($f->ropts && isset($f->ropts['ajax']) && $f->ropts['ajax']) )
$data = $this->fieldData($k);
else
$data = $this->fieldData($k, null, $value);
if( $f->rtype == 'password' ) // Do not show password
$value = null;
return new FormField($value, $f, $error, $data);
}
/**
* Analyzes a relation, internal function
*
* @return array: Associative array with informations about the relation:
* 'ropts' => [ 'refer' => The reference definition, associative array ]
* 'mn' => The n:m Table instance
* 'ncol' => Name of the column referring to my table in NM table's PK
* 'mcol' => Name of the column referring to referred in NM table's PK
*/
private function __analyzeRelation($field) {
if( ! isset($field->ropts['refer']) )
throw new \UnexpectedValueException('Can\'t analize an unspecified relation');
// Use caching to avoid multiple long queries
if( isset($this->__relsCache[$field->name]) )
return $this->__relsCache[$field->name];
// If cache is not available, do the full analysis
$refer = \DoPhp::model($field->ropts['refer']['model']);
$nm = new Table($this->_db, $field->nmtab);
$npk = $this->_table->getPk();
if( count($npk) != 1 )
throw new \dophp\NotImplementedException('Unsupported composed or missing PK');
$npk = $npk[0];
$mpk = $refer->getTable()->getPk();
if( count($mpk) != 1 )
throw new \dophp\NotImplementedException('Unsupported composed or missing PK');
$mpk = $mpk[0];
$ncol = null; // Name of the column referring my table in n:m
$mcol = null; // Name of the column referring other table in n:m
foreach( $nm->getRefs() as $col => list($rtab, $rcol) ) {
if( ! $ncol && $rtab == $this->_table->getName() && $rcol == $npk )
$ncol = $col;
elseif( ! $mcol && $rtab == $refer->getTable()->getName() && $rcol == $mpk )
$mcol = $col;
if( $ncol && $mcol )
break;
}
if( ! $ncol || ! $mcol )
throw new \RuntimeException('Couldn\'t find relations on n:m table ' . $nm->getName() . ' referred by ' . $refer->getTable()->getName());
$nmpk = $nm->getPk();
if( count($nmpk) < 2 )
throw new \RuntimeException('m:m table must have a composite PK');
elseif( count($nmpk) != 2 )
throw new \dophp\NotImplementedException('Unsupported PK in n:m table');
if( array_search($ncol, $nmpk) === false || array_search($mcol, $nmpk) === false )
throw new \RuntimeException('Couldn\'t find columns in relation');
$this->__relsCache[$field->name] = array(
'refer' => $refer,
'nm' => $nm,
'ncol' => $ncol,
'mcol' => $mcol,
);
return $this->__relsCache[$field->name];
}
/**
* Returns the table object instance
*/
public function getTable() {
return $this->_table;
}
/**
* Extracts the primary key from a row and formats it into a string
*
* @param $row array: Associative array, row (must include PK fields)
* @return string: The PK formatted as string
*/
public function formatPk( $row ) {
$pk = $this->_table->getPk();
foreach( $pk as $k )
if( ! isset($row[$k]) )
throw new \UnexpectedValueException("PK Column $k is not part of row");
if( count($pk) < 2 )
return (string)$row[$pk[0]];
$ret = '[';
foreach( $pk as $k )
$ret .= $row[$k] . ',';
rtrim($ret, ',');
$ret .= ']';
return $ret;
}
/**
* Validates form data
*
* @param $post array: POST data
* @param $files array: FILES data
* @param $mode string: Running mode ('insert', 'edit', null if unknown)
* @param $pk mixed: The PK on edit mode, null if unknown (unused, may be used in subclass)
* @see getRules()
* @see \dophp\Validator
*/
public function validate(&$post, &$files, $mode=null, $pk=null) {
$val = new Validator($post, $files, $this->getRules($mode));
return $val->validate();
}
/**
* Returns a short representation of model content, to be used in a select box
* By default, only selects first non-hidden field
*
* @param $filter string: The filter condition as Where instance or the PK
* @param $col string: The name of the column to use as display field
* @return array: Associative array [ \<pk\> =\> \<description\> ], or just \<description\>
* if PK is given
*/
public function summary($filter=null, $col=null) {
if( $col ) {
$cols = $this->_table->getPk();
$cols[] = $col;
} else
$cols = $this->summaryCols();
$pars = new Where();
$pars->add($this->_filter->getRead());
if( $filter instanceof Where ) {
$pars->add($filter);
$pk = false;
} elseif( $filter === null ) {
$pk = false;
} else {
$pars->add($this->_table->parsePkWhere($pk));
$pk = true;
}
$ret = array();
foreach( $this->_table->select($pars, $cols, null, null, $this->_order) as $r )
$ret[$this->formatPk($r)] = $this->summaryRow($r, false, $col);
if( $pk ) {
if( count($ret) > 1 )
throw new \RuntimeException('More than one row returned when filtering by PK');
return array_shift($ret);
}
return $ret;
}
/**
* Returns a string resume of a given data row
*
* @param $row array: The row
* @param $prefix bool: If true, expect column name in the format \<table\>.\<column\>
* @param $prefix string: Prefix prepended to column names in data
* @param $summary string: If given, use this column name as summary column
*/
public function summaryRow(& $row, $prefix=false, $summary=null) {
if( $summary ) {
$displayCol = $summary;
} else {
$sc = $this->summaryCols();
$displayCol = end($sc);
}
if( $prefix )
$prefix = $this->_table->getName() . '.';
else
$prefix = '';
$f = new Field($row["$prefix$displayCol"], $this->_fields[$displayCol]);
return $f->format();
}
/**
* Returns list of columns needed to build a summary, last one is the
* column displayed
*
* @return array: List of column names
*/
public function summaryCols() {
// Use caching
if( ! $this->__sumColsCache ) {
$pks = $this->_table->getPk();
$displayCol = null;
$intCol = null;
foreach( $this->_fields as $n => $f )
if( ! in_array($n,$pks) && $f->rtype )
if( $f->i18n || $this->_table->getColumnType($n) == 'string' ) {
$displayCol = $n;
break;
} elseif( ! $intCol )
$intCol = $n;
if( ! $displayCol )
if( $intCol )
$displayCol = $intCol;
else
$displayCol = $pks[0];
$this->__sumColsCache = $pks;
$this->__sumColsCache[] = $displayCol;
}
return $this->__sumColsCache;
}
/**
* Saves a file, should override
*
* @param $name string: the field name
* @param $data array: File data as returned in $_FILES (\<name\>, \<type\>, \<tmp_name\>, \<error\>, \<size\>)
* @return Mixed: the value to store in the database
*/
protected function _saveFile($name, $data) {
throw new \dophp\NotImplementedException('saveFile not implemented');
}
/**
* Checks if a given record is allowed based on current filter
*
* @param $record array: a query result or post record
* @param $write boolean: if true, use is trying write access, read if false
* @return boolean: True when allowed
*/
protected function isAllowed($record, $write) {
if( ! $this->_filter )
return true;
foreach( $record as $c => $v )
if( ! $this->_filter->isAllowed($c, $v, $write) )
return false;
return true;
}
/**
* Runs custom actions before an item has to be inserted
* does nothing by default, may be overridden
* Database operations run inside a transaction
*
* @param $data array: The data to be inserted, may be modified byRef
* @param $related array: Optional related data
*/
protected function _beforeInsert( & $data, & $related ) { }
/**
* Runs custom actions before an item has to be edited
* does nothing by default, may be overridden
*
* @param $pk mixed: The primary key
* @param $data array: The data to be edited, may be modified byRef
* @param $related array: Optional related data
*/
protected function _beforeEdit($pk, & $data, & $related ) { }
/**
* Runs custom actions before an item has to be deleted
* does nothing by default, may be overridden
*
* @param $pk mixed: The primary key
*/
protected function _beforeDelete($pk) { }
/**
* Runs custom actions after an item has been inserted
* does nothing by default, may be overridden
*
* @param $pk mixed: The primary key
* @param $data array: The data just inserted
* @param $related array: Optional related data
*/
protected function _afterInsert($pk, & $data, & $related ) { }
/**
* Runs custom actions after an item has been edited
* does nothing by default, may be overridden
*
* @param $pk mixed: The primary key
* @param $data array: The data just edited
* @param $related array: Optional related data
*/
protected function _afterEdit($pk, & $data, & $related ) { }
/**
* Runs custom actions after an item has been deleted
* does nothing by default, may be overridden
*
* @param $pk mixed: The primary key
*/
protected function _afterDelete($pk) { }
}
/**
* Defines the characteristics for a field
*
* @deprecated See the new backend, form and field classes
*/
class FieldDefinition {
/**
* string: The database column name for this field.
* Must be omitted only for composed read-only fields
*/
public $name = null;
/** string: The label to display for this field */
public $label = null;
/** string: Long description */
public $descr = null;
/**
* string: The field renderring type in insert/edit mode:
* - \<null\> [or missing]: The field is not rendered at all
* - label: The field is rendered as a label
* - select: The field is rendered as a select box
* - multi: The field is rendered as multiple select box
* - check: The field is rendered as a checkbox
* - auto: The field is renderes as a suggest (autocomplete)
* - text: The field is rendered as a text box
*/
public $rtype = null;
/**
* mixed: If false this field is not rendered in table view.