12.27 php saved
jacwright
Tags add more
ActiveRecord, ActiveRecord Model D, database and model  
Note
This was just done up yesterday and today, so still buggy. But I got inherited statics to work through a hack with back_trace(). Will still need to benchmark it to verify if it is worth it.
  1. <?php
  2.  
  3. /**
  4. * Implementation of the ActiveRecord pattern. Uses the PDO database drivers
  5. * which come with PHP 5.1 and can be installed with PHP 5.0+
  6. *
  7. * This implementation of ActiveRecord determines object relationships on the
  8. * fly and caches the database metadata for optimization. Using the location
  9. * of foreign keys and the plurality of the lookup (e.g. $person->addresses or
  10. * $person->address) ActiveRecord can know how the two objects relate.
  11. *
  12. * In addition, this implementation of ActiveRecord keeps all non-model properties
  13. * out of the class (such as $table, $class, etc) in order to keep the object
  14. * clean for passing to other applications such as webservices or Flash remoting.
  15. *
  16. * The default database schema may be modified for different
  17. * database layouts. Variables which may be included in the strings are:
  18. * %table% %plural_table% %singular_table%
  19. *
  20. * Example of use:
  21. *
  22. * require("ActiveRecord.php");
  23. * ActiveRecord::$db = new PDO("mysql:host=localhost;dbname=mystickies", "root", "");
  24. *
  25. * class Tag extends ActiveRecord {}
  26. *
  27. * class Note extends ActiveRecord {}
  28. *
  29. * $n = Note::findFirst(array("user_id = ?", $_GET['user_id']));
  30. * foreach ($note->tags as $tag) {
  31. *     echo $tag->name;
  32. * }
  33. *
  34. */
  35. abstract class ActiveRecord {
  36.    
  37.     // database layout rules
  38.     public static $tableTransform = "plural";      // plural or singular
  39.     public static $tableFormat = "under_score";      // camelBack, CamelBack (capitalized first letter), or under_score
  40.     public static $pk = "id";                      // other examples: %table%_id, %singular_table%ID
  41.     public static $fk = "%singular_table%_id";      // examples: %table%_id
  42.     public static $joinTable = "%table%_%table%"// examples: %singular_table%%singular_table%
  43.     public static $modifiedField = "updated";      // the standard date field which is set each time the object is saved
  44.     public static $creationField = "created";      // the standard date field which is set when object is first saved
  45.    
  46.     /**
  47.      * PDO Database connection
  48.      * @var PDO
  49.      */
  50.     public static $db; // give ActiveRecord a database connection to work with
  51.    
  52.    
  53.     public $id;
  54.    
  55.     /*****************************************************/
  56.     /* METHODS TO BE OVERRIDDEN                          */
  57.     /*****************************************************/
  58.    
  59.     public function preload() {
  60.        
  61.     }
  62.    
  63.     public function postload() {
  64.        
  65.     }
  66.    
  67.     public function presave() {
  68.        
  69.     }
  70.    
  71.     public function postsave() {
  72.        
  73.     }
  74.    
  75.    
  76.    
  77.     // stores the fields for each table (part of the cache
  78.     protected static $tables;
  79.     protected static $tablesLoaded = false;
  80.     protected static $typeconv = array(
  81.         "date|time" => "date",
  82.         "int|double|decimal|long|short" => "numeric",
  83.         "string|blob",
  84.         "string"
  85.     );
  86.     protected static $prepStmts = array();
  87.    
  88.     /**
  89.      * Constructor of object
  90.      *
  91.      * @param int $id [Optional] If id is present, will load object
  92.      */
  93.     public function __construct($id = null) {
  94.         if (is_numeric($id)) {
  95.             $this->load($id);
  96.         } elseif (is_array($id)) {
  97.             $this->setProperties($id);
  98.         } else {
  99.             $this->setProperties();
  100.         }
  101.     }
  102.    
  103.     /**
  104.      * Loads the object from the database by the id passed
  105.      *
  106.      * @param int $id
  107.      * @return boolean Whether the object was successfully loaded
  108.      */
  109.     public function load($id) {
  110.         $tableName = self::getTableName(get_class($this));
  111.         $pk = self::getPKName($tableName);
  112.         $stmt = self::getStatement("SELECT * FROM $tableName WHERE $pk = ?");
  113.         $stmt->setFetchMode(PDO::FETCH_ASSOC);
  114.         $found = $stmt->execute(array($id));
  115.         if ($found) {
  116.             $row = $stmt->fetch();
  117.             $stmt->closeCursor();
  118.             $this->setProperties($row);
  119.         }
  120.        
  121.         return $found;
  122.     }
  123.    
  124.     /**
  125.      * Loads the object from the database by the conditions passed
  126.      *
  127.      * @param string $conditions Conditions to be passed
  128.      * @param [mixed $properties...]
  129.      * @return boolean Whether the object was succesfully loaded
  130.      */
  131.     public function loadBy($conditions) {
  132.         $this->preload();
  133.        
  134.         $conditionsValues = func_get_args();
  135.         array_shift($conditionsValues); // remove condition string from array
  136.        
  137.         $tableName = self::getTableName(get_class($this));
  138.         $stmt = self::getStatement("SELECT * FROM $tableName WHERE $conditions");
  139.         $stmt->setFetchMode(PDO::FETCH_ASSOC);
  140.         $found = $stmt->execute($conditionsValues);
  141.         if ($found) {
  142.             $row = $stmt->fetch();
  143.             $stmt->closeCursor();
  144.             $this->setProperties($row);
  145.         }
  146.    
  147.         $this->postload();
  148.        
  149.         return $found;
  150.     }
  151.    
  152.     /**
  153.      * Saves the object to the database.
  154.      *
  155.      * @return boolean Whather the object successfully saved
  156.      */
  157.     public function save() {
  158.        
  159.         $this->presave();
  160.        
  161.         $table = self::getTableName(get_class($this));
  162.         $pk = self::getPKName($table);
  163.        
  164.         // set the automatic timestamps
  165.         if (!isset($this->id) && $this->hasProperty(self::$creationField))
  166.             $this->{self::$creationField} = time();
  167.        
  168.         if ($this->hasProperty(self::$modifiedField))
  169.             $this->{self::$modifiedField} = time();
  170.        
  171.         $props = $this->getProperties();
  172.         $prop_values = array_values($props);
  173.         $prop_keys = array_keys($props);
  174.        
  175.         if (isset($this->id)) {
  176.             $stmt = self::getStatement("UPDATE $table SET " .
  177.                 implode(' = ?, ', $prop_keys) .
  178.                 " = ? WHERE $pk = ?"
  179.             );
  180.             $prop_values[] = $this->id;
  181.         } else {
  182.             // TODO generate a new primary key here for databases without autoincrement
  183.             $stmt = self::getStatement("INSERT INTO $table (" .
  184.                     implode(', ', $prop_keys) .
  185.                 ") VALUES (?" .
  186.                     str_repeat(", ?", count($prop_keys) - 1) .
  187.                 ")"
  188.             );
  189.         }
  190.         if (!$stmt->execute($prop_values)) {
  191.             $error = $stmt->errorInfo();
  192.             throw new Exception($error[2]);
  193.         }
  194.        
  195.         if (!isset($this->id)) {
  196.             $this->load(self::$db->lastInsertId());
  197.         }
  198.        
  199.         $this->postsave();
  200.        
  201.         return $success;
  202.     }
  203.    
  204.     public function hasProperty($propName) {
  205.         return array_key_exists($propName, get_object_vars($this));
  206.     }
  207.    
  208.     public function getProperties($join = "") {
  209.         $table = self::getTableName(get_class($this));
  210.         $pk = self::getPKName($table);
  211.         $fields = self::getFields($table);
  212.         $properties = array();
  213.         if ($join) {
  214.             $fields = self::getFields($join);
  215.         }
  216.         foreach ($fields as $field => $info) {
  217.             if ($field == $pk) {
  218.                 continue;
  219.             }
  220.             $value = $this->$field;
  221.            
  222.             if ($info->type == "date" && is_int($value)) {
  223.                 $value = date("Y-m-d H:i:s", $value);
  224.             } elseif ($info->type == "boolean" && is_bool($value)) {
  225.                 $value = $value ? "1" : "0";
  226.             } elseif (is_null($value) && $info->notNull) {
  227.                 continue;
  228.             }
  229.             $properties[$field] = $value;
  230.         }
  231.         return $properties;
  232.     }
  233.    
  234.     public function setProperties($properties = array()) {
  235.         $this->preload();
  236.         $table = self::getTableName(get_class($this));
  237.         $pk = self::getPKName($table);
  238.         $fields = self::getFields($table);
  239.         if (!$fields) {
  240.             return false;
  241.         }
  242.         foreach ($fields as $field => $info) {
  243.             if ($field == $pk) {
  244.                 if (isset($properties[$pk]))
  245.                     $this->id = $properties[$pk];
  246.                 elseif (isset($properties["id"]))
  247.                     $this->id = $properties["id"];
  248.                 continue;
  249.             }
  250.             if (!isset($properties[$field])) {
  251.                 $this->$field = null;
  252.                 continue;
  253.             }
  254.            
  255.             $value = $properties[$field];
  256.             if ($info->type == "date") {
  257.                 $value = preg_match('/^[-0: ]+$/', $value) ? null : strtotime($value);
  258.             } elseif ($info->type == "boolean" && is_numeric($value)) {
  259.                 $value = $value ? true : false;
  260.             }
  261.             $this->$field = $value;
  262.         }
  263.         $this->postload();
  264.     }
  265.    
  266.     /*****************************************************/
  267.     /* STATIC METHODS                                    */
  268.     /*****************************************************/
  269.    
  270.    
  271.     /**
  272.      * Return object found based on id
  273.      *
  274.      * @param string $class The class of the object to load
  275.      * @param int $id The id of the object in the database
  276.      * @return Model Subclass object of type class
  277.      */
  278.     public static function find($id) {
  279.         $class = self::getCaller();
  280.         $pk = self::getPKName(self::getTableName($class));
  281.         $result = self::findAll("$pk = $id", null, 1);
  282.         return $result[0];
  283.     }
  284.    
  285.     /**
  286.      * Returns first object found based on parameters
  287.      *
  288.      * @param string $class The class of the object to load
  289.      * @param string $conditions
  290.      * @param string $order
  291.      * @param string $joins
  292.      * @return Model
  293.      */
  294.     public static function findFirst($conditions = null, $order = null, $joins = null) {
  295.         $result = self::findAll($conditions, $order, 1, $joins);
  296.         return $result[0];
  297.     }
  298.    
  299.     /**
  300.      * Returns array of objects based on parameters
  301.      *
  302.      * @param string $class The class of the objects to load
  303.      * @param string $conditions
  304.      * @param string $order
  305.      * @param string $limit
  306.      * @param string $joins
  307.      * @return array
  308.      */
  309.     public static function findAll($conditions = "", $order = "", $limit = "", $joins = "") {
  310.         $class = self::getCaller();
  311.         $table = self::getTableName($class);
  312.         $pk = self::getPKName($table);
  313.        
  314.         // get the fields cached in advance
  315.         self::getFields($table);
  316.        
  317.         //sql (make sure we have the original table's pk last so if there are joins, they don't interfere
  318.         $sql = "SELECT *, $table.$pk FROM $table";
  319.         $stmt = self::executeSQL($sql, $conditions, $limit, $order, $joins);
  320.        
  321.         $i = 0;
  322.         $num = 0;
  323.         $result = array();
  324.         if ($limit) {
  325.             $limit = explode(',', $limit);
  326.             $total = $limit[0];
  327.             $i = isset($limit[1]) ? $limit[1] : 0;
  328.         }
  329.         while ($row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, $i++)) {
  330.             $result[] = new $class($row);
  331.             if (++$num == $total)
  332.                 break;
  333.         }
  334.         $stmt->closeCursor();
  335.         return $result;
  336.     }
  337.    
  338.     /**
  339.      * Returns array of objects based on the full sql statement
  340.      *
  341.      * @param string $class The class of the objects to load
  342.      * @param string $sql
  343.      * @param array $params
  344.      * @param string $limit
  345.      * @return array
  346.      */
  347.     public static function findBySql($sql, $params = null, $limit = "") {
  348.         $class = self::getCaller();
  349.         $stmt = self::getStatement($sql, array(PDO::ATTR_CURSOR, PDO::CURSOR_SCROLL));
  350.         if (!$stmt->execute($params)) {
  351.             $error = $stmt->errorInfo();
  352.             throw new Exception($error[2]);
  353.         }
  354.        
  355.         $i = 0;
  356.         $num = 0;
  357.         $total = 0;
  358.         $result = array();
  359.         if ($limit) {
  360.             $limit = explode(',', $limit);
  361.             $i = $limit[0];
  362.             $total = isset($limit[1]) ? $limit[1] : 0;
  363.         }
  364.         while ($row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, $i++)) {
  365.             $result[] = new $class($row);
  366.             if (++$num == $total) break;
  367.         }
  368.         $stmt->closeCursor();
  369.         return $result;
  370.     }
  371.    
  372.     /**
  373.      * Returns whether or not the object exists in the database
  374.      *
  375.      * @param string $class The class of the objects to load
  376.      * @param int $id
  377.      * @return boolean
  378.      */
  379.     public static function exists($id) {
  380.         $class = self::getCaller();
  381.         $table = self::getTableName($class);
  382.         $pk = self::getPKName($table);
  383.         return (self::count("$pk = $id") > 0);
  384.     }
  385.    
  386.     /**
  387.      * Creates new object, populates the attributes from the array,
  388.      * saves it if it validates, and returns it
  389.      *
  390.      * @param string $class The class of the object to create
  391.      * @param array $properties
  392.      * @return Model
  393.      */
  394.     public static function create($properties = null) {
  395.         $class = self::getCaller();
  396.         $obj = new $class($properties);
  397.         $obj->save();
  398.         return $obj;
  399.     }
  400.    
  401.     /**
  402.      * Updates an object already stored in the database with the properties passed
  403.      *
  404.      * @param string $class The class of the object to update
  405.      * @param int $id The id of the class in the database
  406.      * @param string/array $properties
  407.      * @return boolean Whether it was successfully updated
  408.      */
  409.     public static function update($id, $properties) {
  410.         $class = self::getCaller();
  411.         $table = self::getTableName($class);
  412.         $pk = self::getPKName($table);
  413.         // the properties element should be the same format as conditions
  414.         $properties = self::prepareConditions($properties);
  415.         $stmt = self::getStatement("UPDATE $table SET " . array_shift($properties) . " WHERE $pk = ?");
  416.         array_push($properties, $id);
  417.         return $stmt->execute($properties);
  418.     }
  419.    
  420.     /**
  421.      * Updates all records with properties by conditions
  422.      *
  423.      * @param string $class The class of the objects to update
  424.      * @param string $conditions
  425.      * @param array $properties
  426.      * @return int Number of successful updates
  427.      */
  428.     public static function updateAll($conditions = null, $properties = null) {
  429.         $class = self::getCaller();
  430.         $table = self::getTableName($class);
  431.        
  432.         $properties = self::prepareConditions($properties);
  433.         $conditions = self::prepareConditions($conditions);
  434.         $sql = "UPDATE $table SET " . array_shift($properties);
  435.         $stmt = self::executeSQL($sql, array_merge(array_splice($conditions, 0, 1), $properties, $conditions));
  436.         return $stmt->rowCount();
  437.     }
  438.    
  439.     /**
  440.      * Delete object by id
  441.      *
  442.      * @param string $class The class of the object to delete
  443.      * @param int $id The id of the object in the database
  444.      * @return boolean Whether object was deleted
  445.      */
  446.     public static function delete($id) {
  447.         $class = self::getCaller();
  448.         $table = self::getTableName($class);
  449.         $pk = self::getPKName($table);
  450.         $stmt = self::getStatement("DELETE FROM $table WHERE $pk = ?");
  451.         return $stmt->execute($id);
  452.     }
  453.    
  454.     /**
  455.      * Deletes all records by conditions
  456.      *
  457.      * @param string $class The class of the objects to delete
  458.      * @param string $conditions
  459.      * @param string $limit
  460.      * @param string $deleteFrom Tables to delete records from
  461.      * @param string $joins Table joins needing to be added
  462.      * @return int Number of successful deletes
  463.      */
  464.     public static function deleteAll($conditions = null, $deleteFrom = null, $joins = null) {
  465.         $class = self::getCaller();
  466.         $table = self::getTableName($class);
  467.        
  468.         if (!$deleteFrom) {
  469.             $deleteFrom = $table;
  470.         }
  471.         //sql
  472.         $sql = "DELETE $deleteFrom FROM $table";
  473.         $stmt = self::executeSQL($sql, $conditions, null, null, $joins);
  474.         return $stmt->rowCount();
  475.     }
  476.    
  477.     /**
  478.      * Returns the number of records that meet the conditions
  479.      *
  480.      * @param string $class
  481.      * @param string $conditions
  482.      * @param string $joins
  483.      * @return int
  484.      */
  485.     public static function count($conditions = null, $joins = null) {
  486.         $class = self::getCaller();
  487.         $table = self::getTableName($class);
  488.         $sql = "SELECT COUNT(*) FROM $table";
  489.         $stmt = self::executeSQL($sql, $conditions, null, null, $joins);
  490.         // return first row, first field
  491.         $stmt->setFetchMode(PDO::FETCH_COLUMN, 0);
  492.         $count = $stmt->fetch();
  493.         $stmt->closeCursor();
  494.         return $count;
  495.     }
  496.    
  497.     /**
  498.      * Returns the number of records returned by the sql statement
  499.      *
  500.      * @param string $class
  501.      * @param string $sql
  502.      * @param array $params
  503.      * @return int
  504.      */
  505.     public static function countBySql($sql, $params = null) {
  506.         $class = self::getCaller();
  507.         $stmt = self::getStatement($sql);
  508.         if (!$stmt->execute($params)) {
  509.             $error = $stmt->errorInfo();
  510.             throw new Exception($error[2]);
  511.         }
  512.         // return first row, first field
  513.         $stmt->setFetchMode(PDO::FETCH_COLUMN, 0);
  514.         $count = $stmt->fetch();
  515.         $stmt->closeCursor();
  516.         return $count;
  517.     }
  518.    
  519.     /**
  520.      * Increment a property in a Model class
  521.      *
  522.      * @param string $class
  523.      * @param int $id
  524.      * @param string $counter The property of the class to be incremented
  525.      */
  526.     public static function incrementCounter($id = null, $counter = null) {
  527.         self::update($id, array("$counter = $counter + 1"));
  528.     }
  529.    
  530.     /**
  531.      * Decrements a counter in a record
  532.      *
  533.      * @param string $class
  534.      * @param int $id
  535.      * @param string $counter The property of the class to be decremented
  536.      */
  537.     public static function decrementCounter($id = null, $counter = null) {
  538.         self::update($id, array("$counter = $counter - 1"));
  539.     }
  540.    
  541.    
  542.    
  543.     //TODO EVERYTHING BELOW THIS
  544.    
  545.    
  546.    
  547.     // MAGIC METHODS FOR MODEL ******************************************
  548.    
  549.     /**
  550.      * Catches all methods called and forwards on certain types to their
  551.      * defined method. e.g. $this->doSomething(10) will call
  552.      * $this->_do("Something", 10) if the _do method is defined.
  553.      *
  554.      * @param string $func The name of the method
  555.      * @param array $args The arguments passed to it
  556.      * @return mixed The return value of the resolved method
  557.      */
  558.     protected function __call($func, $args) {
  559.         preg_match('/(^[^A-Z]*)([A-Z].*)/', $func, $matches);
  560.         $catchFunc = '_' . $matches[1];
  561.         // push the property name to the front of the args array
  562.         array_unshift($args, lcfirst($matches[2]));
  563.        
  564.         if (method_exists($this, $catchFunc)) {
  565.             return call_user_func_array(array($this, $catchFunc), $args);
  566.         } else {
  567.             throw new Exception("Method " . get_class($this) . "::$func() does not exist.");
  568.         }
  569.     }
  570.    
  571.     /**
  572.      * Catches all set actions to undefined properties and assumes they are related
  573.      * objects. Tries to save related object or array of objects.
  574.      *
  575.      * @param string $property Name of unset property we are setting.
  576.      * @return mixed The object or array of objects we are trying to set.
  577.      */
  578.     protected function __set($property, $value) {
  579.         $this->_set($property, $value);
  580.     }
  581.    
  582.     /**
  583.      * Catches all get requests to undefined properties and assumes they are related
  584.      * objects. Tries to find and load related object or array of objects.
  585.      *
  586.      * @param string $property Name of unset property we are getting.
  587.      * @return mixed The object or array of objects we are trying to get (or null if not found).
  588.      */
  589.     protected function __get($property) {
  590.         return $this->_get($property);
  591.     }
  592.    
  593.    
  594.     /**
  595.      * Does automatic date conversion (to int) for database date properties and
  596.      * save relational objects on the fly. Calls $this->loadProperty which
  597.      * may be provided by subclass if custom loading is required
  598.      *
  599.      * @param string $prop
  600.      * @param mixed $value
  601.      * @param unknown_type $force
  602.      */
  603.     protected function _set($prop, $value) {
  604.         $table = self::getTableName(get_class($this));
  605.         $fields = self::getFields($table);
  606.         if (isset($fields[$prop]) && $fields[$prop]->type == "date" && is_string($value)) {
  607.             $value = strtotime($value);
  608.         }
  609.        
  610.         $this->$prop = $value;
  611.         /*
  612.         // $this->$prop could be set if _set is reached through $this->setProperty($value);
  613.         if ((is_object($value) || is_array($value)) && !isset($this->$prop)) {
  614.             $this->{"save" . ucfirst($prop)}();
  615.         }*/
  616.     }
  617.    
  618.     /**
  619.      * Loads relational objects on the fly. Calls $this->loadProperty which
  620.      * may be provided by subclass if custom loading is required
  621.      *
  622.      * @param string $property Property to get
  623.      * @return mixed The value returned from this object
  624.      */
  625.     protected function _get($property) {
  626.         $table = self::getTableName(get_class($this));
  627.         if (!isset($this->$prop) && self::getFields($table)) {
  628.             $this->{"load" . ucfirst($property)}();
  629.         }
  630.         return $this->$property;
  631.     }
  632.    
  633.     /**
  634.      * Adds an object to an array and saves relational objects on the fly
  635.      *
  636.      * @param string $property
  637.      * @param mixed $value
  638.      */
  639.     protected function _add($property, $value) {
  640.         $property = Inflector::pluralize($property);
  641.         if (!isset($this->$property)) {
  642.             $this->{"load" . ucfirst($property)}();
  643.         }
  644.         array_push($this->$property, $value);
  645.         $this->{'save' . ucfirst($property)}();
  646.     }
  647.    
  648.     /**
  649.      * Removes an object from an array and removes the relationship of relational objects
  650.      *
  651.      * @param string $property
  652.      * @param obj/int $objOrNum
  653.      * @return mixed Returns the object removed from the array
  654.      */
  655.     protected function _remove($property, $objOrNum) {
  656.         $property = Inflector::pluralize($property);
  657.         if (!isset($this->$property)) {
  658.             $this->{"load" . ucfirst($property)}();
  659.         }
  660.         // TODO save object after removing it (save this or that object, depending on where Fkey is)
  661.         if (is_numeric($objOrNum)) {
  662.             $removed = array_splice($this->$property, $objOrNum, 1);
  663.             return $removed[0];
  664.         }
  665.  
  666.         foreach ($this->$property as $index => $tempObj) {
  667.             if ($tempObj === $objOrNum) {
  668.                 $removed = array_splice($this->$property, $index, 1);
  669.                 return $removed[0];
  670.             }
  671.         }
  672.         return false;
  673.     }
  674.    
  675.     /**
  676.      * Gives the size of an array property, loading relational objects on the fly
  677.      *
  678.      * @param string $property
  679.      * @return int
  680.      */
  681.     protected function _sizeof($property) {
  682.         if ($this->relations[$property] && !isset($this->$property))
  683.             $this->{'load' . ucfirst($property)}();
  684.         if (!is_array($this->$property))
  685.             $this->$property = array();
  686.         return sizeof($this->$property);
  687.     }
  688.    
  689.    
  690.     // Database automatic methods
  691.    
  692.     protected function _load($property, $conditions = null, $order = null, $limit = null) {
  693.         if (isset($this->$property)) {
  694.             return $this->$property;
  695.         }
  696.        
  697.         if (!isset($this->id)) {
  698.             return false;
  699.         }
  700.