Smutty

MVC Framework

View Code: Smutty_Model

Browse: All · Classes · Plugins

1
<?php
2
3
// required data types
4
define( 'STR_REQUIRED', 1 );
5
define( 'INT_REQUIRED', 2 );
6
define( 'DATE_REQUIRED', 3 );
7
// optional data types
8
define( 'INT_OPTIONAL', 4 );
9
define( 'STR_OPTIONAL', 5 );
10
define( 'DATE_OPTIONAL', 6 );
11
12
/**
13
* this is the base class for all Smutty models. it provides
14
* many methods and utilities which (hopefully) make using
15
* models the joy it should be.
16
*
17
*/
18
19
class Smutty_Model extends Smutty_Object {
20
21
/** model data */
22
private $_data;
23
24
/** cache for properties */
25
private $_propertyCache;
26
27
/** the name of this models table, defaults to false so it will be
28
worked out dynamically, but you can set it to the actual table
29
name if needed. */
30
public $tableName = false;
31
32
/** allows specifying dependent models by their name. the related
33
field is assumed to be {model}_id, but you can specify it if
34
you need to by using dot notation like so:
35
var $hasOne = 'User.myuservar';
36
multiple entries should be space seperated. */
37
public $hasMany = '';
38
public $hasOne = '';
39
public $hasRelation = '';
40
41
/** this property should be used to specify how data is to
42
be validated. it should be a hash of the name of the
43
property and then a constant indicating it's type and
44
it's "requiredness" */
45
public $validate = array();
46
47
/** keeps track of changes to the model so we know whether we
48
need to go expend the overhead of actually validating */
49
private $_needsValidating;
50
51
/** enable rss output for this model */
52
public $rss = '';
53
54
/** any errors on this model */
55
private $errors = array();
56
57
/**
58
* constructor
59
*
60
* @return nothing
61
*
62
*/
63
64
public function __construct() {
65
$this->_data = new stdclass();
66
$this->_propertyCache = array();
67
$this->errors = array();
68
$this->_needsValidating = true;
69
}
70
71
/**
72
* this method tries to automatically fill a
73
* model with data that has been passed to
74
* the application.
75
*
76
* @return nothing
77
*
78
*/
79
80
public function fill() {
81
82
$class = get_class( $this );
83
$db = Smutty_Database::getInstance();
84
$data = Smutty_Data::getInstance();
85
$session = Smutty_Session::getInstance();
86
$hash = $data->data( strtolower($class) );
87
$fields = Smutty_Model::getFields( $class );
88
89
foreach ( $fields as $field ) {
90
91
$name = $field->name;
92
93
// try and fill field with data
94
if ( $value = $hash->string($field->name) )
95
$this->$name = $value;
96
97
// maybe it's a date field?
98
elseif ( preg_match('/^date/',$field->type) ) {
99
// see if we have special smutty date fields
100
if ( $data->exists($field->name . '_year') ) {
101
$year = $data->string( $field->name . '_year' );
102
$month = $data->string( $field->name . '_month' );
103
$day = $data->string( $field->name . '_day' );
104
// only set field if we have all the data
105
if ( $year && $month && $day )
106
$this->$name = date(
107
$db->getDateFormat(),
108
strtotime("$year-$month-$day")
109
);
110
}
111
else $this->$name = $data->getDate();
112
}
113
114
// otherwise a user field?
115
elseif ( $session->user && ($field->name == 'user_id') )
116
$this->$name = $session->user->id;
117
118
}
119
120
}
121
122
/**
123
* populates the model with data from the array
124
*
125
* @param hash $data a hash of name/value pairs
126
* @return nothing
127
*
128
*/
129
130
private function _populate( $data ) {
131
foreach ( $data as $key => $value )
132
$this->$key = $value;
133
}
134
135
/**
136
* returns the name of the table this model is
137
* associated with.
138
*
139
* @param String $class class to fetch table for
140
* @return String table name
141
*
142
*/
143
144
private static function _getTable( $class ) {
145
$model = class_exists( $class )
146
? new $class() : new stdclass();
147
if ( $model->tableName )
148
return $model->tableName;
149
else
150
return Smutty_Inflector::tableize( $class );
151
}
152
153
/**
154
* returns an array of this models fields and their types
155
*
156
* @param String $class the class to fetch fields for
157
* @return array fields for this model
158
*
159
*/
160
161
public static function getFields( $class ) {
162
163
static $cache;
164
if ( $cache == null )
165
$cache = array();
166
167
$cacheId = $class;
168
if ( !isset($cache[$cacheId]) ) {
169
170
$db = Smutty_Database::getInstance();
171
$tblName = Smutty_Model::_getTable( $class );
172
$sql = $db->getFieldsSql( $tblName );
173
$res = $db->query( $sql );
174
$array = array();
175
176
while ( $row = $res->fetch() ) {
177
$field = new stdclass();
178
$field->name = $row[ 0 ];
179
$field->nullable = ( strtolower($row[2]) == 'yes' );
180
// strip field sizes, just want the basic type
181
$field->type = preg_replace( '/^(.*)\(.*$/', '$1', $row[1] );
182
$array[] = $field;
183
}
184
185
$cache[$cacheId] = $array;
186
187
}
188
189
return $cache[ $cacheId ];
190
191
}
192
193
/**
194
* returns the total number of records for the model
195
*
196
* @param String $class class to fetch for
197
* @return int total records
198
*
199
*/
200
201
public static function getTotal( $class ) {
202
203
$db = Smutty_Database::getInstance();
204
$table = Smutty_Model::_getTable( $class );
205
$sql = " select count(*) as total
206
from `$table` ";
207
if ( !$res = $db->query($sql) )
208
Smutty_Error::fatal( $db->getError(), 'ClassSmutty_Database' );
209
$row = $res->fetchObject();
210
211
return $row->total;
212
213
}
214
215
/**
216
* returns a reference to the "find cache"
217
*
218
* @return hashRef reference to find cache hash
219
*
220
*/
221
222
private static function &_getFindCache() {
223
static $cache;
224
if ( $cache == null )
225
$cache = array();
226
return $cache;
227
}
228
229
/**
230
* this function allows searching for a record by
231
* it's unique id. if it is found then an instance
232
* of it's model object will be returned, otherwise
233
* you'll get false.
234
*
235
* @param String $class the model class to search
236
* @param String $id the id to find
237
* @param String $field the field to find on (default='id')
238
* @return Smutty_Model the model found
239
*
240
*/
241
242
public static function find( $class, $id, $field = 'id' ) {
243
244
$cache =& self::_getFindCache();
245
$cacheId = strtolower($class) . $id;
246
247
if ( !isset($cache[$cacheId]) ) {
248
249
$db = Smutty_Database::getInstance();
250
$tblName = Smutty_Model::_getTable( $class );
251
$id = $db->escape( $id );
252
$field = $db->escape( $field );
253
$sql = " select *
254
from `$tblName`
255
where `$field` = '$id' ";
256
if ( !$res = $db->query($sql) )
257
Smutty_Error::fatal( $db->getError(), 'ClassSmutty_Model' );
258
if ( $row = $res->fetchAssoc() ) {
259
$model = new $class();
260
$model->_populate( $row );
261
$cache[$cacheId] =& $model;
262
}
263
else $cache[$cacheId] = false;
264
265
}
266
267
return $cache[ $cacheId ];
268
269
}
270
271
/**
272
* returns all the records for this model
273
*
274
* @param String $class the class to search
275
* @param String $order order the results by eg. "field:desc"
276
* @param array $params name/value pairs for fields to match
277
* @param String $limit how to limit results eg. "start:count"
278
* @param array $joins array or string of model joins to make
279
* @return array Model objects
280
*
281
*/
282
283
public static function fetchAll( $class, $order = false, $params = array(), $limit = false, $whereSql = '', $joins = array() ) {
284
285
$model = new $class();
286
$db = Smutty_Database::getInstance();
287
288
// specify an order by?
289
$orderSql = '';
290
if ( $order ) {
291
$parts = explode( ':', $order );
292
$orderSql = " order by `$class`.`$parts[0]` " .
293
( isset($parts[1]) ? " $parts[1] " : 'asc' );
294
}
295
296
// limit results?
297
$limitSql = '';
298
if ( $limit ) {
299
$parts = explode( ':', $limit );
300
$limitSql = isset($parts[1])
301
? " limit $parts[0], $parts[1] "
302
: " limit $parts[0] ";
303
}
304
305
// other params
306
$whereSql = $whereSql ? " and $whereSql " : '';
307
if ( is_array($params) )
308
foreach ( $params as $key => $value ) {
309
$ops = explode( ':', $key );
310
$name = preg_replace( '/\./', '`.`', $ops[0] );
311
$op = isset($ops[1]) ? $ops[1] : '=';
312
$value = $db->escape( $value );
313
switch ( $op ) {
314
case 'contains':
315
$whereSql = " and ( match ($name) against ('$value') ) ";
316
break;
317
case 'like':
318
default:
319
$whereSql .= " and ( `$name` $op '$value' ) ";
320
}
321
}
322
$whereSql = substr( $whereSql, 4 );
323
if ( $whereSql ) $whereSql = " where $whereSql ";
324
325
// work out joins
326
$joinSql = '';
327
$joins = is_array($joins) ? $joins : explode( ' ', $joins );
328
foreach ( $joins as $join )
329
$joinSql .= self::getJoinSql( $class, $join );
330
331
// generate sql for query
332
$tblName = Smutty_Model::_getTable( $class );
333
$sql = " select `$class`.*
334
from `$tblName` `$class`
335
$joinSql
336
$whereSql
337
$orderSql
338
$limitSql ";
339
if ( !$res = $db->query($sql) )
340
Smutty_Error::fatal( $db->getError(), 'ClassSmutty_Model' );
341
342
// put results in array ready to return
343
$array = array();
344
while ( $row = $res->fetchAssoc() ) {
345
$model = new $class();
346
$model->_populate( $row );
347
$array[] = $model;
348
}
349
350
return $array;
351
352
}
353
354
/**
355
* returns the sql to do a join to the dot seperated
356
* list of models specified. an inner join is used.
357
*
358
* eg. getJoinSql( 'Post', 'User.UserType' );
359
*
360
* @param String $fromAlias the alias to join from
361
* @param String $joins dot seperated string of models
362
*
363
*/
364
365
private static function getJoinSql( $fromAlias, $joins ) {
366
367
$joinSql = '';
368
$parts = explode( '.', $joins );
369
$alias = array_shift( $parts );
370
371
if ( $alias ) {
372
373
$table = self::_getTable( $alias );
374
$field = w( $parts, 1, strtolower($alias.'_id') );
375
376
$joinSql = " inner join `$table` `$alias` " .
377
" on `$alias`.`id` = `$fromAlias`.`$field` " .
378
self::getJoinSql( $alias, implode('.',$parts) );
379
380
}
381
382
return $joinSql;
383
384
}
385
386
/**
387
* this function checks if the models data is currently in a
388
* valid state to be saved. this means that all validation
389
* criteria that have been specified need to be met.
390
*
391
* errors are stored in $this->errors and can be accessed
392
* by using the $this->getErrors() function.
393
*
394
* @return boolean indicates validity
395
*
396
*/
397
398
public function isValid() {
399
400
$errors = array();
401
$fields = Smutty_Model::getFields( get_class($this) );
402
403
foreach ( $fields as $field ) {
404
405
$name = $field->name;
406
$value = is_object($this->$name) ? $this->$name->id : $this->$name;
407
408
// check requiredness from db
409
if ( ($name != 'id') && (!isset($value) && !$field->nullable) )
410
array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
411
412
// try validate array
413
elseif ( $rules = v($this->validate,$name) ) {
414
415
$rules = is_array($rules) ? $rules : array( 'type' => $rules );
416
417
// check by type
418
if ( $type = v($rules,'type') )
419
switch ( $type ) {
420
case DATE_REQUIRED:
421
if ( !$value )
422
array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
423
break;
424
case STR_REQUIRED:
425
if ( !$value )
426
array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
427
break;
428
case INT_REQUIRED:
429
$value = Smutty_Data::getInt( $value );
430
if ( !$value )
431
array_push( $errors, " $name " . ERR_VALIDATE_REQUIRED );
432
break;
433
}
434
435
// check by regexp
436
if ( $regexp = v($rules,'regexp') )
437
if ( !preg_match($regexp,$value) )
438
array_push( $errors, " $name is in an invalid format" );
439
440
// check by max length
441
$length = strlen( $value );
442
if ( $max = v($rules,'maxlength') )
443
if ( $length > $max )
444
array_push( $errors, " $name is too long" );
445
446
// check for min length
447
if ( $min = v($rules,'minlength') )
448
if ( $length < $min )
449
array_push( $errors, " $name is too short" );
450
451
}
452
453
// is there a validate method defined?
454
$method = 'validate' . ucfirst($name);
455
if ( method_exists($this,$method) ) {
456
$data = Smutty_Data::getInstance();
457
$session =& Smutty_Session::getInstance();
458
if ( $error = $this->$method($value,$data,$session) )
459
array_push( $errors, $error );
460
}
461
462
}
463
464
// set errors on current controller
465
Smutty_Controller::addErrors( $errors );
466
467
$this->errors = $errors;
468
$this->_needsValidating = ( $errors );
469
return ( !$this->_needsValidating );
470
471
}
472
473
/**
474
* returns an array containing any errors that were encountered the
475
* last time the model was validated.
476
*
477
* @return array array of error strings
478
*
479
*/
480
481
public function getErrors() {
482
483
return $this->errors;
484
485
}
486
487
/**
488
* determines if a record with the specified id
489
* currently exists in this models table
490
*
491
* @param String $id the id to search for
492
* @return boolean indicates if id exists
493
*
494
*/
495
496
public function exists( $id ) {
497
$db = Smutty_Database::getInstance();
498
$table = Smutty_Model::_getTable( get_class($this) );
499
$id = $db->escape( $id );
500
$sql = " select 1
501
from `$table`
502
where id = '$id' ";
503
$res = $db->query( $sql );
504
return ( $res->fetch() );
505
}
506
507
/**
508
* returns the sql for UPDATING this models record
509
*
510
* @return String the sql
511
*
512
*/
513
514
private function _getUpdateSql() {
515
516
$class = get_class( $this );
517
$db = Smutty_Database::getInstance();
518
$table = Smutty_Model::_getTable( $class );
519
$sql = '';
520
$fields = Smutty_Model::getFields( $class );
521
522
foreach ( $fields as $field ) {
523
$name = $field->name;
524
if ( $name == 'id' )
525
continue;
526
$value = is_object($this->$name) ? $this->$name->id : $this->$name;
527
$value = $db->escape( $value );
528
$sql .= ", `$name` = '$value' ";
529
}
530
531
$sql = substr( $sql, 1 );
532
$id = $db->escape( $this->id );
533
$sql = " update `$table`
534
set $sql
535
where id = '$id' ";
536
537
return $sql;
538
539
}
540
541
/**
542
* returns a value properly formatted for the specified field type
543
*
544
* @param String $field a field (name/value) object
545
* @param String $value the desired field value
546
* @return String the field value
547
*
548
*/
549
550
private function _getFieldValue( $field, $value ) {
551
$db = Smutty_Database::getInstance();
552
if ( !$db ) Smutty_Error::fatal( ERR_DB_NO_CONN, 'ClassSmutty_Database' );
553
switch ( $field->type ) {
554
case 'int':
555
return (int) $value;
556
break;
557
case 'datetime':
558
//check date
559
if ( !(preg_match('/^\d{4}-\d{2}-\d{2}$/',$value) || preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/',$value)) )
560
return '';
561
// fall through...
562
default:
563
return $db->escape( $value );
564
}
565
}
566
567
/**
568
* returns the sql for inserting a new record with this
569
* models data.
570
*
571
* @return String the sql
572
*
573
*/
574
575
private function _getInsertSql() {
576
577
$class = get_class( $this );
578
$db = Smutty_Database::getInstance();
579
$sqlFields = '';
580
$sqlValues = '';
581
$fields = Smutty_Model::getFields( $class );
582
$table = Smutty_Model::_getTable( $class );
583
584
foreach ( $fields as $field ) {
585
$name = $field->name;
586
if ( $name == 'id' )
587
continue;
588
$value = is_object($this->$name) ? $this->$name->id : $this->$name;
589
$value = $this->_getFieldValue( $field, $value );
590
$sqlFields .= ", $field->name";
591
$sqlValues .= ", '$value' ";
592
}
593
594
$sqlFields = substr( $sqlFields, 1 );
595
$sqlValues = substr( $sqlValues, 1 );
596
$sql = " insert into `$table`
597
( $sqlFields )
598
values ( $sqlValues ); ";
599
600
return $sql;
601
602
}
603
604
/**
605
* this function saves a models data, it returns a boolean
606
* indicating if the save went ahead ok or not.
607
*
608
* @return boolean if save succeeded
609
*
610
*/
611
612
public function save() {
613
614
$class = get_class( $this );
615
$db = Smutty_Database::getInstance();
616
$sql = null;
617
618
// first we need to do any data validation that is required
619
if ( $this->_needsValidating && !$this->isValid() )
620
return false;
621
622
// existing record or new one?
623
if ( $this->id && $this->exists($this->id) )
624
$sql = $this->_getUpdateSql();
625
else
626
$sql = $this->_getInsertSql();
627
628
if ( $db->update($sql) ) {
629
// auto-set an id?
630
if ( !$this->id )
631
$this->id = $db->getInsertId( $this->_getTable($class) );
632
// as this model has been edited we need
633
// to remove it from the find() cache
634
$cache =& self::_getFindCache();
635
unset( $cache[get_class($this).$this->id] );
636
return true;
637
}
638
else return false;
639
640
}
641
642
/**
643
* deletes the current model from the database
644
*
645
* @return boolean if delete succeeded
646
*
647
*/
648
649
public function delete() {
650
651
$db = Smutty_Database::getInstance();
652
$class = get_class( $this );
653
$table = Smutty_Model::_getTable( $class );
654
$sql = " delete from `$table`
655
where `id` = '$this->id' ";
656
return $db->update( $sql );
657
658
}
659
660
/**
661
* this method allows deleting of multiple records
662
*
663
* @param String $class the class to delete for
664
* @param array $params assoc array of where params
665
* @return nothing
666
*
667
*/
668
669
public static function deleteWhere( $class, $params ) {
670
671
$db = Smutty_Database::getInstance();
672
$table = Smutty_Model::_getTable( $class );
673
$whereSql = '';
674
675
// create where sql
676
foreach ( $params as $name => $value )
677
$whereSql .= ' and ( `' . $name . '` = \'' . $db->escape($value) . '\' ) ';
678
$whereSql = substr( $whereSql, 4 );
679
if ( $whereSql )
680
$whereSql = ' where ' . $whereSql;
681
682
// do the delete
683
$sql = " delete from `$table`
684
$whereSql ";
685
return $db->update( $sql );
686
687
}
688
689
/**
690
* checks the model for a custom property handler for
691
* the specifed property name, if it's found then it's
692
* executed and it's result returned.
693
*
694
* @param String $name name of property
695
* @return mixed property value
696
*
697
*/
698
699
private function _tryPropertyHandler( $name ) {
700
701
// first check for a custom property handler
702
$propertyMethod = $name . 'Property';
703
704
if ( method_exists($this,$propertyMethod) ) {
705
706
// set properties on model directly, we need to do
707
// a little trickery... this is pretty messy but
708
// i can't think of a better way to do it right now.
709
foreach ( $this->_data as $key => $value )
710
$this->$key = $value;
711
// now call property handler
712
$result = $this->$propertyMethod();
713
// then remove the properties we set (otherwise
714
// they won't trigger __get() calls)
715
foreach ( $this->_data as $key => $value )
716
unset( $this->$key );
717
718
return $result;
719
720
}
721
722
return null;
723
724
}
725
726
/**
727
* checks the $hasOne property to see if there is
728
* a mapping defined that matches the current property
729
* that we're looking for.
730
*
731
* @param String $name name of the property
732
* @return String property value
733
*
734
*/
735
736
private function _tryHasOneMapping( $name ) {
737
738
if ( !$this->hasOne ) return;
739
740
$parts = explode( ' ', $this->hasOne );
741
742
foreach ( $parts as $part ) {
743
$names = explode( '.', $part );
744
$modelName = $names[0];
745
$tableName = strtolower( $modelName );
746
$propName = w( $names, 2, $tableName );
747
// have we found a related field we need to load?
748
if ( $name == $propName ) {
749
$fieldName = w( $names, 1, $tableName . '_id' );
750
$id = isset($this->_data->$fieldName) ? $this->_data->$fieldName : '';
751
$model = false;
752
if ( class_exists($modelName) )
753
$model = Smutty_Model::find( $modelName, $id );
754
return $model;
755
}
756
}
757
758
return null;
759
760
}
761
762
/**
763
* checks the $hasMany property to see if there is
764
* a mapping defined that matches the current property
765
* that we're looking for.
766
*
767
* @param String $name name of the property
768
* @return mixed property value
769
*
770
*/
771
772
private function _tryHasManyMapping( $name ) {
773
774
if ( !$this->hasMany ) return;
775
776
$parts = explode( ' ', $this->hasMany );
777
foreach ( $parts as $part ) {
778
$names = explode( '.', $part );
779
$modelName = $names[ 0 ];
780
$fieldName = w( $names, 1, strtolower(get_class($this)) . '_id' );
781
$orderBy = w( $names, 2, 'id:asc' );
782
$plural = Smutty_Inflector::pluralize(
783
strtolower($modelName)
784
);
785
if ( $name == $plural ) {
786
$id = $this->_data->id;
787
$models = false;
788
if ( class_exists($modelName) )
789
$models = Smutty_Model::fetchAll(
790
$modelName, $orderBy, array( $fieldName => $id )
791
);
792
return $models;
793
}
794
}
795
796
return null;
797
798
}
799
800
/**
801
* checks the model to see if there are any "relation" mapping
802
* defined. these are mapping that use a relation table to
803
* define many/many relations/
804
*
805
* @param String $name name of property
806
* @return mixed array of models
807
*
808
*/
809
810
private function _tryHasRelationMapping( $name ) {
811
812
$relations = split( ' ', $this->hasRelation );
813
814
foreach ( $relations as $relation ) {
815
// get relation info
816
$parts = split( '\.', $relation );
817
$model = v( $parts, 0 );
818
$modelPlural = Smutty_Inflector::pluralize( strtolower($model) );
819
// have we found a relation match?
820
if ( $name == $modelPlural ) {
821
822
$db = Smutty_Database::getInstance();
823
$class = strtolower(get_class($this));
824
$table = $class . '_' . $modelPlural;
825
826
// query for id's from relation table
827
$idString = ' -1 ';
828
$fromId = $class . '_id';
829
$toId = strtolower($model) . '_id';
830
$sql = " select $toId
831
from $table
832
where $fromId = '" . $this->_data->id . "' ";
833
$res = $db->query( $sql );
834
while ( $row = $res->fetch() )
835
$idString .= " , '" . $db->escape($row[0]) . "' ";