· 8 years ago · Oct 24, 2017, 10:46 PM
1<?php
2/**
3 * ======
4 * FastFM
5 * ======
6 * FastFM is a PHP ORM for Filemaker backends heavily inspired
7 * by Rails' Active Model / Active Record combination.
8 *
9 * FastFM REQUIRES the official Filemaker PHP API to work.
10 * FastFM is just an extension of the official API, NOT
11 * a replacement.
12 *
13 * FastFM is a lightweight yet powerful ORM that allows for easy
14 * management of records as well as PHP model validation.
15 *
16 * @author: James Strong
17 * @copyright: None!
18 * @license: Do whatever you want with this. No restrictions whatsoever.
19 * @version: 1.0 Alpha.
20 *
21 * ===============
22 * Getting Started
23 * ===============
24 * FastFM requires little to no setup. All you
25 * need to do is include the official filemaker api
26 * as well as this PHP file, set up database credentials and
27 * your models then you're good to go!
28 *
29 * Start by including the libraries required:
30 *
31 * // Official Filemaker PHP API
32 * require_once '/lib/FileMaker.php'
33 * // FastFM Filemaker ORM
34 * require_once '/lib/FastFM.php'
35 *
36 * Now you have to set up a connection to the filemaker database
37 * through the FilemakerDB singleton class (part of the framework)
38 *
39 * FilemakerDB::setup(host, database, username, password);
40 *
41 * Define your model ORM classes and make them extend from FastModel.
42 * Example:
43 * class User extends FastModel {
44 * // Every model must have a default layout
45 * // to read from.
46 * public static $layout = 'users';
47 *
48 * // FastModel uses a default unique id of 'id'.
49 * // but can be overrided by defining the static
50 * // id variable in the child class.
51 * public static $id = 'user_id';
52 *
53 * public function __construct() {
54 * $this -> validation = array('name' => Validation::$PRESENCE);
55 * }
56 * }
57 *
58 * And that's about it! You should be able to query filemaker through the User class.
59 * User::find('13') // searches for row in filemaker with user_id = 13
60 *
61 * ============
62 * CRUD queries
63 * ============
64 * FastFM has 5 functions that can be used for creating,
65 * reading, updating and deleting records.
66 *
67 * All standard FileMaker operators for newFindCommand
68 * can be used.
69 *
70 * Any Filemaker Errors that are thrown by the query are thrown as
71 * Model Exceptions.
72 *
73 * -- Find (static) --
74 * Find is used for searching for models through
75 * their unique identifier (defined as public static $id on your FastModel).
76 *
77 * If no record could be found (Filemaker Error 401),
78 * the find command will return null.
79 *
80 *
81 * Examples:
82 * // searches for user with user_id of 1.
83 * User::find('1')
84 *
85 * // searches for user with user_id of 2
86 * // and uses the CompactUser layout to
87 * // reduce the number of fields returned.
88 * User::find('2', 'CompactUser')
89 *
90 * // Throws ModelException.
91 * User::find('error')
92 *
93 *
94 * -- Where (static) --
95 * Similar to find, but instead searches based on the array of criteria
96 * passed into the function.
97 *
98 * Examples:
99 * // Returns an array of Users who have "Strong" in their name.
100 * User::where(array('name' => 'Strong'));
101 *
102 * // Returns a User object from the first record that has the email
103 * // james.strong@etherpros.com
104 * User::where(array('email' => 'james.strong@etherpros.com'), true);
105 *
106 *
107 *
108 * -- Create (static) --
109 * Receives a list of field/value parameters and creates a new FastModel
110 * object as well as a database record based on those parameters.
111 *
112 * Example:
113 * User::create(array('name' => 'James Strong', 'email' => 'james.strong@etherpros.com'));
114 *
115 *
116 * -- All (static) --
117 * Performs a standard FindAllCommand and returns an array of your ORM object.
118 *
119 * Example:
120 * User::all();
121 *
122 *
123 * -- Update --
124 * Updates FilemakerDB with changes done to model.
125 * Returns true or false.
126 *
127 * Example:
128 * $user = User::find('1');
129 * $user -> email = 'updated@email.com';
130 * $user -> update();
131 *
132 *
133 * -- Update Attributes --
134 * Same as Update, but receives a list of attributes
135 * to update instead of updating dirty fields.
136 *
137 * Example:
138 * $user = User::find('1');
139 * $user -> update_attributes(array('email' => 'updated@email.com'));
140 *
141 *
142 *
143 * -- Save --
144 * Performs update() if model points to an existing database entry.
145 * Performs create() if model is a new model.
146 *
147 * Examples:
148 * (create example)
149 * $user = new User();
150 * $user -> name = "James Strong";
151 * $user -> email = 'james.strong@etherpros.com';
152 * $user -> save();
153 *
154 *
155 * ==========
156 * Validation
157 * ==========
158 * FastFM has some basic support for validation.
159 * Not very extensive at the moment, but still useful.
160 *
161 * FastFM supports these 3 validation methods out of the box.
162 * Validation::$PRESENCE
163 * Validation::$UNIQUENESS
164 * // email regexp check.
165 * Validation::$EMAIL_FORMAT
166 *
167 * In order to use these methods you have to define
168 * the validation array in the constructor of your FastFM class.
169 *
170 * Example:
171 * class User {
172 * public function __construct() {
173 * $this -> validation = array('name' => Validation::$PRESENCE,
174 * 'email' => array(Validation::$PRESENCE, Validation::$EMAIL_FORMAT),
175 * 'username' => Validation::$UNIQUENESS);
176 * }
177 * }
178 *
179 * The array key is the field you want to validate on and the value is the validation function.
180 * In order to have more than one validation for a given field
181 * you must pass in a sub-array as a value (see email example).
182 *
183 *
184 * If you need more validation, you can define your own custom functions to be validated on.
185 * Validation::$CUSTOM_FUNCTION = 'function on your model'
186 *
187 * The function defined on your model receives one parameter, the validation object on which
188 * you add errors per your validation.
189 *
190 * Example:
191 * Validation::$CUSTOM_FUNCTION = 'username_no_spaces'
192 *
193 * class User {
194 * // Checks if there are blank spaces inside of username
195 * public function username_no_spaces($validation) {
196 * $pos = strpos($value, ' ');
197 * // if some spaces were found
198 * if ($pos){
199 * $this -> addError("Username", "Username can't have spaces");
200 * return false;
201 * }
202 * }
203 * }
204 *
205 * ==========
206 * That's it!
207 * ==========
208 * That's it for now!
209 * But don't worry, FastFM is under active development
210 * and there will be more updates soon!
211 *
212 * You can follow me on github (username laspluviosillas)
213 * if you are interested in contributing.
214 */
215
216 public function checkForSpaces($value) {
217 $pos = strpos($value, ' ');
218 if ($pos){
219 $this -> addError("Username","Username can't have spaces");
220 return false;
221 }
222 return true;
223 }
224
225//== code starts here ==
226/**
227 * ==========
228 * Fast Model
229 * ==========
230 * Base class that all ORM Models extend from.
231 */
232class FastModel {
233
234 /**
235 * Default Filemaker layout that will be queried
236 * for this class. Example: TrainSession.class.php
237 * has a default layout of TrainSession.
238 */
239 public static $layout;
240
241 /**
242 * Primary field of the object in Filemaker.
243 * Used for the find function.
244 * Example: TrainSession -> uniqueID = 'SessionID'.
245 */
246 public static $id = 'id';
247
248 /** FileMaker DB fields are stored in this hash. **/
249 public $fields = array();
250
251 /**
252 * Hash that contains fields mapped to their validation functions
253 * to be used by the Validation.
254 *
255 * Example:
256 * array('firstName' => Validation::$PRESENCE,
257 * 'lastName' => Validation::$PRESENCE,
258 * 'email' => array(Validation::$EMAIL_FORMAT, Validation::$UNIQUENESS),
259 * );
260 */
261 public $validation;
262
263 /** Contains validation errors after a call to validate(). **/
264 public $errors;
265
266 /** Determines whether model already exists in the database or not **/
267 public $persisted = false;
268
269
270 /**
271 * Original FilemakerRecord id from fin queries.
272 * Keeping this object in memory prevents an extra find query
273 * from being needed on the update() and save() functions.
274 **/
275 public $record_id;
276
277 /** Stores variables that have been updated,
278 * Used when saving or updating object in order
279 * to only send the variables that need to be updated
280 * instead of the entire object.
281 **/
282 public $dirty = array();
283
284 /** Instantiates variables for any attributes sent in **/
285 function __construct($attributes = array()) {
286 foreach($attributes as $key => $value) {
287 $this -> {$key} = $value;
288 }
289 }
290
291
292 /** Dynamic getters and setters for filemaker fields **/
293 public function __get($name) {
294 return $this -> fields[$name];
295 }
296
297 public function __set($name, $value) {
298 // set field to dirty if model is persisted and field has changed.
299 if($this -> fields[$name] != $value) {
300 $this -> dirty[$name] = true;
301 }
302
303 $this -> fields[$name] = $value;
304 }
305
306 /** Resets dirty tracking array.
307 Called after an update or save **/
308 private function clean_dirt() {
309 $this -> dirty = array();
310 }
311
312 /** Returns true if dirty array is not of size 0 **/
313 public function is_dirty() {
314 return sizeof($this -> dirty) != 0;
315 }
316
317 /**
318 * Returns validation object based on the 'validation' hash.
319 */
320 public function validate() {
321 $validation = new Validation();
322 $validation -> setModel($this);
323 $validation -> validate();
324 $this -> errors = $validation -> getErrors();
325 return $validation;
326 }
327
328 /** Returns boolean value if the model doesn't have
329 any errors from its last validation check through
330 validate().
331 */
332 public function is_valid() {
333 return sizeof($this -> errors) == 0;
334 }
335
336 /** Saves changed fields on object to the database. **/
337 public function update() {
338 // throw an exception if the object is a new object
339 // and doesn't correspond to an already-existing database
340 // record.
341 if(!$this -> persisted) {
342 throw new ModelException('Update cannot be called on non-persisted objects');
343 return true;
344 }
345
346 // if the object is not dirty, there is nothing to update!
347 if(!$this -> is_dirty()) {
348 return true;
349 }
350
351 // validate values before update!
352 $this -> validate();
353
354 // return false if invalid because update failed.
355 if(!$this -> is_valid()) {
356 return false;
357 }
358
359 $query = new FilemakerQuery(static::$layout);
360
361 // Get record for this object.
362 // Use record_id if present since it is a faster search
363 // otherwise use unique id defined on the table.
364 // Which is not the same as the Filemaker unique id, strangely enough.
365 if($this -> record_id != null) {
366 $record = $query -> findById($this -> record_id);
367 } else {
368 // If no record_id, do standard find command on model's unique id.
369 $id = $this -> get_id();
370 $record = $query -> findFirst(array(static::$id =>$id)) -> getFirstRecord();
371 }
372
373 // serialize object to array to send into
374 // filemaker query.
375 $criteria = $this -> to_dirty_array();
376
377 // perform update
378 $query = new FilemakerQuery(static::$layout);
379 $query -> update($criteria, $record);
380
381 // clean dirty array since model was just updated.
382 $this -> clean_dirt();
383
384 // everything was successful! (in theory).
385 return true;
386 }
387
388 /** Extension of update() above. Receives a hash of attributes
389 * populates the model with those attributes and then calls
390 * update().
391 */
392 public function update_attributes($attributes) {
393 $this -> set_attributes($attributes);
394 return $this -> update();
395 }
396
397 /** Save is really just an alias for create and update.
398 * Create if model is not persisted.
399 * Update if it is.
400 *
401 * Example:
402 *
403 * Create example:
404 * $login = new Login();
405 * $login -> name = 'James Strong'
406 * $login -> email = 'james.strong@etherpros.com'
407 * $login -> save();
408 * // returns true or false.
409 *
410 * Update example:
411 * $login = Login::find('1');
412 * $login -> name = 'I have a new name!';
413 * $login -> save();
414 * // also returns true or false.
415 **/
416 public function save() {
417 // if persisted just route to update();
418 if($this -> persisted) {
419 return $this -> update();
420 } else {
421 // perform create.
422 // In the case of create, since dirty-tracking
423 // is not applicable, the entire fields array
424 // is sent as criteria to be saved.
425 $criteria = $this -> fields;
426 $this -> validate();
427 if($this -> is_valid()) {
428 $query = new FilemakerQuery(static::$layout);
429 $query -> create($criteria);
430 return true;
431 }
432 //if invalid return false
433 return false;
434 }
435 }
436
437 /** Static create. Calls sets attributes on a new ORM model instance and then calls save() on it.
438 * Example:
439 * $login = Login::create('name' => 'James Strong', 'email' => 'james.strong@etherpros.com');
440 * if($login -> is_valid()) {
441 * echo 'login created';
442 * } else {
443 * print_r($login -> errors);
444 * }
445 **/
446 public static function create($criteria) {
447 $instance = self::instantiate();
448 $instance -> set_attributes($criteria);
449 $instance -> save();
450 return $instance;
451 }
452
453 private function set_attributes($attributes) {
454 foreach($attributes as $key => $value) {
455 // It is important to set the attribute using
456 // $this -> {$key} syntax and NOT by setting
457 // the fields array directly like $this -> fields[$key].
458 // The latter method calls the __set function which includes
459 // dirty tracking, while the former skips all the dirty tracking
460 // checks.
461 $this -> {$key} = $value;
462 }
463 }
464
465 /** Returns array of all objects **/
466 public static function all($layout = null) {
467 if($layout == null) {
468 $layout = static::$layout;
469 }
470 $query = new FilemakerQuery($layout);
471 $records = $query -> all() -> getRecords();
472 return self::unserialize_many($records);
473 }
474
475 /**
476 * Performs a filemaker find command based on the ID passed in.
477 * Searches on the primary field (defined by uniqueID) of the layout.
478 * Uses default layout for the model unless one is specified.
479 * Example:
480 * $login = Login::find('301');
481 */
482 public static function find($id, $layout = null) {
483 if($layout == null) {
484 $layout = static::$layout;
485 }
486 $query = new FilemakerQuery($layout);
487 $record = $query -> findFirst(array(static::$id => $id)) -> getFirstRecord();
488 return self::unserializer($record);
489 }
490
491 /**
492 * Performs a filemaker find command based on the hash of criteria passed in.
493 * $firstOnly returns only the first record if set to true.
494 * $layout overrides default layout to class.
495 *
496 * Example:
497 * $logins = Login::where(array('FirstName' => 'James', 'Status' => 'Active'));
498 */
499 public static function where($criteria, $firstOnly=false, $layout=null) {
500 if($layout == null) {
501 $layout = static::$layout;
502 }
503 $query = new FilemakerQuery($layout);
504 if($firstOnly) {
505 $record = $query -> findFirst($criteria) -> getFirstRecord();
506 // return unserialized object
507 return self::unserializer($record);
508 }else{
509 $records = $query -> find($criteria) -> getRecords();
510 // Just return an empty array if no records were found
511 if($records == null) { return array(); };
512 return self::unserialize_many($records);
513 }
514 }
515
516 /** Unserializes an array of Filemaker_Result objects **/
517 private static function unserialize_many($records) {
518 $results_array = array();
519 foreach($records as $record) {
520 array_push($results_array, self::unserializer($record));
521 }
522 return $results_array;
523 }
524
525
526 /**
527 * Static version of unserialize method below.
528 *
529 * NOTE:
530 * For some completely unknown reason, PHP
531 * doesn't allow for there two be 2 methods with the
532 * same name even though one is a static method
533 * and the other is an instance method.
534 *
535 * Example:
536 * private static function unserialize()
537 * private function unserialize()
538 * - throws error message -
539 *
540 * Due to this naming restriction, I had to
541 * name the class (static) level unserialize method
542 * 'unserializer' instead,
543 *
544 **/
545 private static function unserializer($record) {
546 $instance = self::instantiate();
547 $instance -> unserialize($record);
548 return $instance;
549 }
550
551 /** Unserializes Filemaker_Result object and populates fields hash. **/
552 private function unserialize($record) {
553 $fields = $record -> getFields();
554 foreach($fields as $field) {
555 $this -> fields[$field] = $record -> getField($field);
556 }
557
558 // filemaker unique record id.
559 $this -> record_id = $record -> getRecordId();
560
561 // set instance to persisted since it was unserialized
562 // from the database.
563 $this -> persisted = true;
564 }
565
566 /** Serializes all ORM fields to a a field name / value hash of changed fields.
567 * Function is used to do update commands from an
568 * ORM object to Filemaker.
569 * Only returns dirty fields.
570 */
571 public function to_dirty_array() {
572 $serialized = array();
573 foreach($this -> dirty as $key => $value) {
574 $serialized[$key] = $this -> fields[$key];
575 }
576 return $serialized;
577 }
578
579 /** Creates new instance of this object from the static scope **/
580 public static function instantiate() {
581 $class = static::klass();
582 $instance = new $class();
583 return $instance;
584 }
585
586 /** Simple alias for retrieving unique id **/
587 public function get_id() {
588 return $this -> {static::$id};
589 }
590
591 /** Simple alias for retrieving the class name **/
592 public static function klass() {
593 return get_called_class();
594 }
595
596}
597
598/**
599 *
600 * ===============
601 * Filemaker Query
602 * ==============
603 * Filemaker Query is just a helper class that
604 * adds a layer of abstraction for performing filemaker commands.
605 * This class is heavily used by FastModel for its basic ORM queries.
606 *
607 */
608class FilemakerQuery {
609 private static $VALID = "VALID";
610 private static $BLANK = "BLANK";
611 private static $ERROR = "ERROR";
612
613 public $connection = null;
614 public $layout = null;
615 public $sortRules = null;
616
617 public function __construct($layout) {
618 $this -> connection = FilemakerDB::getConnection();
619 $this -> layout = $layout;
620 }
621 public function setSortRules($sortRules) {
622 $this -> sortRules = $sortRules;
623 }
624
625 /** Basic update command. Receives a hash of parameters
626 * and returns a Filemaker_Result object.
627 * Example:
628 * $query = new FilemakerQuery('Logins');
629 * $criteria = array('FullName' => 'James Strong');
630 * $record = $query -> findFirst($criteria) -> getFirstRecord();
631 * $query -> update(array('FullName' => 'John Strong'));
632 */
633 public function update($criteria, $record) {
634 foreach($criteria as $key=>$value) {
635 //echo $key . " " . $value . "\n";
636 $record -> setField($key, $value);
637 }
638 $result = $record -> commit();
639 return $result;
640 }
641
642 /** Basic create command. Receives a hash of parameters
643 * and returns a Filemaker_Result object.
644 * Example:
645 * $query = new FilemakerQuery('NewLMSCopy');
646 * $query -> create(array("Login_ID" => 'id value'));
647 */
648 public function create($criteria) {
649 $record =& $this -> connection -> newAddCommand($this -> layout, array());
650 foreach($criteria as $key => $value) {
651 $record -> setField($key, $value);
652 }
653 $result = $record -> execute();
654 return $result;
655 }
656
657 /** Same as find command, just with a max cap of one **/
658 public function findFirst($criteria) {
659 return $this -> find($criteria, 1);
660 }
661
662 /** Performs a Find All FileMaker command **/
663 public function all() {
664 $findCommand =& $this -> connection -> newFindAllCommand($this -> layout);
665 $result = $findCommand -> execute();
666 $this -> validateResult($result);
667 return $result;
668 }
669
670 /** Equivalent to FileMaker's getRecordById function **/
671 public function findById($id) {
672 $result = $this -> connection -> getRecordById($this -> layout, $id);
673 $this -> validateResult($result);
674 return $result;
675 }
676
677 /** Basic find command. Receives a hash of parameters
678 * and returns a Filemaker_Result object.
679 *
680 * $criteria: Hash of values used for the find command.
681 * $max: Max number of records to be retrieved. -1 means no cap.
682 *
683 * Compound finds are performed by passing in arrays of arrays.
684 *
685 * Examples:
686 * $query = new FilemakerQuery('Logins');
687 * $result = $query -> find(array('LastName' => 'Strong')
688 *
689 * Compound find example:
690 * $query = new FilemakerQuery('TrainSession');
691 * $criteria = array(
692 * array("Login_ID" => "=".$loginID, "Status" => "Assigned"),
693 * array("Login_ID" => "=".$loginID, "Status" => "Live")
694 * );
695 * $result = $query -> find($criteria, -1);
696 */
697 public function find($criteria, $max = -1, $loud=false) {
698 // if the array is multi-dimensional, this is a compound find.
699 $compound = is_array($criteria[0]);
700
701 if(!$compound) {
702 $findCommand =& $this -> connection -> newFindCommand($this -> layout);
703 //if not a compound find command, then add the criteria directly to the find command.
704 foreach($criteria as $key => $value) {
705 $findCommand -> addFindCriterion($key, $value);
706 }
707 } else {
708 $findCommand =& $this -> connection -> newCompoundFindCommand($this -> layout);
709 //if a compound find command, then create find requests, add criteria to the requests.
710 //then add the requests to the find command.
711 $i = 0;
712 foreach($criteria as $set) {
713 $i++;
714 $findRequest =& $this -> connection -> newFindRequest($this -> layout);
715 foreach($set as $key => $value) {
716 $findRequest -> addFindCriterion($key, $value);
717 }
718 $findCommand -> add($i, $findRequest);
719 }
720 }
721 // Set max number of records if present.
722 if($max != -1) {
723 $findCommand -> setRange(0, $max);
724 }
725 // Set sort rules.
726 if($this -> sortRules) {
727 $i = 0;
728 foreach($this -> sortRules as $key => $value) {
729 $i++;
730 $findCommand -> addSortRule($key, 1, $value);
731 }
732 }
733 $result = $findCommand -> execute();
734 //HACK: return empty result object if no records found instead of a filemaker error.
735 if($this -> validateResult($result) == self::$BLANK && $loud == false) {
736 return new Filemaker_Result($this -> connection);
737 }
738 return $result;
739 }
740
741 /** Checks Filemaker_Result to make sure it is not an error.
742 * When loud is set to true, this function will throw
743 * an exception if the result is an error.
744 *
745 * When loud is set to false, the function will
746 * only return true or false depending on whether
747 * the result is an error or not.
748 *
749 * Error 401 has been overriden to return BLANK instead of ERROR.
750 * Exceptions are only thrown for critical errors that shouldn't happen.
751 * On the other hand, 401 errors can happen all the time if a search
752 * query doesn't return any results. This is normal behavior and should not throw an exception.
753 */
754 public function validateResult($result, $loud=true) {
755 $isError = FileMaker::isError($result);
756 if($isError == true) {
757 // 401 is a no record found error, so we don't want to throw a model exception for this.
758 if ( $result->getCode() == "401" ) {
759 return self::$BLANK;
760 }else{
761 //only throw an exception if this is a loud validation
762 if($loud) {
763 throw new ModelException("Error retrieving information from database: " . $result -> getCode(),
764 $result -> getCode());
765 }
766 return self::$ERROR;
767 }
768 }
769 if($isError == false) {
770 return self::$VALID;
771 }
772 }
773}
774
775/**
776* ============
777* Filemaker DB
778* ============
779* Singleton that lazy-loads connection to FileMaker DB
780**/
781class FilemakerDB {
782 // database connection details
783 public static $host = 'localhost';
784 public static $database = null;
785 public static $username = '';
786 public static $password = '';
787
788 private static $connection = null;
789
790 // Private constructor for singleton pattern
791 private function __construct() {
792 }
793
794 /** Values required for setting up the connection. **/
795 public static function setup($host, $database, $username, $password) {
796 self::$host = $host;
797 self::$database = $database;
798 self::$username = $username;
799 self::$password = $password;
800 }
801
802 // lazy-loads filemaker database connection
803 public static function getConnection() {
804 if (!self::$connection)
805 {
806 self::$connection = new FileMaker();
807 self::$connection->setProperty('database', self::$database);
808 self::$connection->setProperty('hostspec', self::$host);
809 self::$connection->setProperty('username', self::$username);
810 self::$connection->setProperty('password', self::$password);
811
812 }
813 return self::$connection;
814 }
815
816 // clone private for singleton pattern
817 private function __clone(){
818 }
819}
820
821/* Stub exception class for Filemaker Model exceptions */
822class ModelException extends Exception {}
823
824
825
826/**
827 *
828 * ===========
829 * Validation
830 * ===========
831 * This class is used for model-level validation.
832 * It also is able to generate a generic validation view.
833 *
834 * This class is heavily used by ModelBase for validating models
835 * before persisting them to the database.
836 *
837 */
838class Validation {
839 public static $PRESENCE = "presenceValidation";
840 public static $UNIQUENESS = "uniquenessValidation";
841 public static $EMAIL_FORMAT = "emailFormatValidation";
842
843 /*
844
845 $CUSTOM_FUNCTION is an exception.
846 All of the constants defined above call functions that are defined in this class.
847
848 $CUSTOM_FUNCTION instead calls a function defined in the MODEL being validated.
849
850 Pseudo-code example:
851 class MyCustomClass extends FastModel {
852 public static $validation = array(Validation::$CUSTOM_FUNCTION, 'iWillAlwaysFail');
853 public function iWillAlwaysFail($validation) {
854 $validation -> addError('Always Fail', 'I am a custom function that always fails');
855 }
856 }
857
858 When validate() is called on an instance of MyCustomClass,
859 iWillAlwaysFail which is defined in our MyCustomClass will be called,
860 and the validation will return "I am a custom function that always fails".
861
862 */
863 public static $CUSTOM_FUNCTION = "CustomValidationFunctionForModel";
864
865 private $model = null;
866 private $action = null;
867 private $errors = array();
868
869 /** Goes through the validation array inside of $model
870 * and calls the requires validation functions.
871 *
872 * Refer to FastModel for details on how the validation hash is defined.
873 *
874 * Example:
875 * $attendee = new Attendee();
876 *
877 * // Is going to fail, because Attendee requires
878 * // first name and last name presence.
879 * $attendee -> setFirstName('');
880 * $attendee -> setLastName('');
881 * $attendee -> setEmail('totally@unique.com');
882 *
883 * $validation = new Validation();
884 * $validation -> setModel($login);
885 * $validation -> validate();
886 *
887 * // Returns First Name and Last Name can't be blank.
888 */
889 public function validate() {
890 foreach($this -> model -> validation as $key => $value) {
891 if( is_array($value) ) {
892 foreach ($value as $entry) {
893 $this -> validation_call($key, $entry);
894 }
895 }else{
896 $this -> validation_call($key, $value);
897 }
898 }
899 }
900
901 private function validation_call($key, $value) {
902 // if the model is persisted to the database
903 // then we only want to call validation on
904 // dirty fields, since we know that non-dirty fields are valid.
905 // So, if the model is persisted but the field being validated is NOT
906 // dirty, skip validation call.
907 if($this -> model -> persisted && $this -> model -> dirty[$key] != true) {
908 return;
909 }
910
911 if($key != self::$CUSTOM_FUNCTION) {
912 $this -> $value($key);
913 } else {
914 $this -> model -> $value($this);
915 }
916 }
917
918 private function uniquenessValidation($key) {
919 $value = $this -> model -> { $key };
920 // uniqueness validation does NOT manage blank values.
921 // doing a record search with a blank value returns all records.
922 // so the uniqueness will always fail.
923 if(!empty($value)) {
924 $class = $this -> model -> klass();
925 $this -> checkUniquenessFor($key, $value, $class::$layout);
926 }
927 }
928 private function presenceValidation($key) {
929 $value = $this -> model -> {$key};
930 $this -> checkPresence($key, $value);
931 }
932 private function emailFormatValidation($key) {
933 $value = $this -> model -> {$key};
934 $this -> checkEmail($value);
935 }
936
937 public function validatePasswordEquality($pwd, $pwd_confirm) {
938 // if password values do not match
939 if($pwd != $pwd_confirm) {
940 //send error of passwords not matching.
941 $this -> addError('Password', "Password and Password confirmation fields do not match.");
942 return false;
943 }
944 return true;
945 }
946 public function checkPresence($key, $value) {
947 if(empty( $value )) {
948 $this -> addError($key, $key . " can't be blank");
949 return false;
950 }
951 return true;
952 }
953
954 public function checkEmail($email) {
955 if(!empty($email)) {
956 $result = TRUE;
957 if(!eregi("^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$", $email)) {
958 $result = FALSE;
959 }
960 if(!$result) {
961 $this -> addError('Email', "Email is not a valid format.");
962 }
963 return $result;
964 }
965 }
966
967 public function checkUniquenessFor($key, $value, $table) {
968 //escape @ characters
969 $value = str_replace("@", "\@", $value);
970 $query = new FilemakerQuery($table);
971 $records = $query -> findFirst(array($key => "=".$value)) -> getRecords();
972 $record_count = 0;
973 foreach ((array) $records as $record) {
974 $record_count++;
975 }
976 //return true of false depending on whether field is already present in the DB.
977 $unique = ($record_count > 0) ? false : true;
978 if(!$unique) $this -> addError($key, "This " . $key . " is already taken.");
979 return $unique;
980 }
981
982 // ===================
983 // Getters and Setters
984 // ===================
985 public function setModel($model) { $this -> model = $model; }
986 public function getModel() { return $model; }
987
988 public function getErrors() {
989 return $this->errors;
990 }
991 public function addError($key,$value) {
992 array_push($this->errors, new ValidationError($key, $value));
993 }
994 public function isValid() {
995 return sizeof($this->errors) == 0;
996 }
997 public function numberOfErrors() {
998 return sizeof($this->errors);
999 }
1000 public function setErrors($errors) {
1001 $this->errors = $errors;
1002 }
1003
1004}
1005
1006class ValidationError {
1007 public $key = "";
1008 public $value = "";
1009 public function __construct($key,$value) {
1010 $this -> key = $key;
1011 $this -> value = $value;
1012 }
1013}