<?php
/***************************************************************
* ARASPHP WEB DEVELOPING FRAMEWORK
*
* Website: www.arasphp.org
* Author: Arturo López Pérez
* hide@address.com
* Version: 0.02
***************************************************************
*
* This file it's part of ArasPhp Web developing framework.
*
* This project is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This project is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/*
* Model
* Base to all app models.
* Can handle ANY table dinamically. It can
*
* 1) Fetch information as object
* 2) Insert
* 3) Update
* 4) Delete
*
* It handles flexible auto-validation
*
* And many more useful things =)
*
* (!)You can put your own shared methods in /core/AppModel.php
*/
abstract class Model extends ErrorLogger {
// Validation errors
var $validationErrors = NULL;
// Possible relationships
var $possibleRelationships = array("hasMany","hasOne","belongsTo");
/**
* CONSTRUCTOR
* It has two ways
* 1) From an Id. It will fetch all it's attributes from the database
* 2) From an assoacciative array.
*
* @param arg. Can be numeric or array
*/
function __construct($arg) {
/*
* CASE 1: SELF-CONSTRUCT FROM THE DATABASE
* $arg is numeric
*/
if(is_numeric($arg))
{
// Our id
$this->id = $arg;
// Assign our out attributes
self::refresh();
// Out!
return;
}
/*
* CASE 2: ASSOCIATIVE ARRAY RECEIVED TO CONSTRUCT A NEW OBJECT
* $arg is associative array.
*
* > Parameters >= required and <= required+optional
* > All REQUIRED fields must be provided
* > OPTIONAL fields may or may not be provided
*/
if(is_array($arg))
{
// Our fields
$required = self::getRequiredFields();
$optional = self::getOptionalFields();
$forbidden = self::getForbiddenFields();
/*
* CHECK 1: Are the number of parameters correct?
*/
if(!(count($arg) >= count($required) &&
count($arg) <= (count($required) + count($optional))))
{
parent::logError("Number of fields passed do not fit the model requirements.");
return;
}
/*
* CHECK 2: Are all required parameters satisfied?
*/
foreach($required as $requiredField)
{
$satisfied = false;
// Is this required parameter in our array?
foreach($arg as $field => $value)
{
if($requiredField == $field)
{
$satisfied = true;
}
}
// Outa here, if satisfied is still false, this parameter is not satisfied
if($satisfied == false)
{
parent::logError("Missing field `$requiredField` for model `" . get_class($this) . "`");
}
}
if(parent::failed())
return;
/*
* CHECK 3: Are the missing parameters to validate optional parameters?
* Any unknown parameter will cause an error
*/
foreach($arg as $field => $value)
{
// A parameter can only be a required or optional
if(!(in_array($field, $required) || in_array($field, $optional)))
{
parent::logError("Unknown field `$field` for model `" . get_class($this) . "`");
}
}
if(parent::failed())
return;
/*
* CHECK 4: Is there any forbidden field provided?
*/
foreach($arg as $field => $value)
{
if(in_array($field, $forbidden))
{
parent::logError("Forbidden field `$field` provided for model `" . get_class($this) . "`");
}
}
if(parent::failed())
return;
/*
* We got here!
* Everything good so far
* Let's self assign
*/
foreach($arg as $key => $value)
{
$this->$key = $value;
}
// If desired by the model, we validate ourselves.
if($this->autoValidation == true)
self::validate();
// Everthing cool. Out!
return;
}
// We get here in case arg passed to the constructor is not numeric or array
parent::logError("Passed parameters does not fit any of the valid constructor arguments. Only and integer or an assoaciative array are allowed.");
} // CONSTRUCTOR END
/*
* DATABASE QUERIES OPERATION WITH THE MODEL
* > Insert
* > Update
* > Delete
*
* Uses helpers methods:
* > getUpdateQuery
* > getInsertQuery
* > getDeleteQuery
*/
/**
* The insert method inserts a new row in the database and fetches it's id to his own id attribute
* Checks that related before and after methods exists, if not, error and out.
*/
public function insert()
{
// Populate date_created and time_stamp
self::populateDateCreated();
self::populateTimeStamp();
// We check that beforeInsert() and afterInsert() methods exist
$traceBefore = self::executeRelatedMethods($this->beforeInsert, true);
$traceAfter = self::executeRelatedMethods($this->afterInsert, true);
if($traceBefore == false || $traceAfter == false)
{
parent::logError("The insertion hasn't been performed due related methods errors.");
return;
}
// If we got here we will execute the before methods
self::executeRelatedMethods($this->beforeInsert);
// If any of those is false, the insertion will not be executed
$result = mysql_query(self::getInsertQuery());
if(mysql_error())
{
// We add error log here
parent::logError("Insert into $this->table failed! Database said: " . mysql_error());
} else {
// If no mysql_error and everything is cool...
$this->id = mysql_insert_id();
// Execute the after methods
self::executeRelatedMethods($this->afterInsert);
}
}
/**
* This method is a bit tricky. It will execute or simulate the execution of a bunch of methods passed in array
* They will be used for beforeInsert, afterInsert, beforeUpdate, beforeDelete and so on methods...
*
* @param simulate enabled will just return wether the methods exists or not
* @return true -> everything ok
* false -> some method does not exist
*/
private function executeRelatedMethods($methodsCollection, $simulate = false)
{
// If we have something to execute
if(isset($methodsCollection) && is_array($methodsCollection))
{
// Then we check they all exist before executing any of them
foreach($methodsCollection as $method)
{
if(!method_exists($this, $method))
{
parent::logError("The method `$method` does not exist for the model `" . get_class($this) . "`");
}
}
// If any errors ocurred, we are out
if(parent::failed()) {
return false;
} else {
// If no error has been thrown and only simulation was asked, return true
if($simulate == true) { return true; }
}
// Got here! Anything failed. Now let's execute them
foreach($methodsCollection as $method)
{
$this->$method();
}
// Everything executed
return true;
// If there is nothing to execute, return true as everything is cool anyway
} else {
return true;
}
}
/**
* Update all it's data
* Updates every row of itself with the stored values in the object
* Executes beforeUpdate and afterUpdate methods as well
*/
public function update()
{
// Populate date_modify if exists
self::populateDateModify();
// We check that beforeUpdate() and afterUpdate() methods exist
$traceBefore = self::executeRelatedMethods($this->beforeUpdate, true);
$traceAfter = self::executeRelatedMethods($this->afterUpdate, true);
if($traceBefore == false || $traceAfter == false)
{
parent::logError("The update hasn't been performed due related methods errors.");
return;
}
// If we got here we will execute the before methods
self::executeRelatedMethods($this->beforeUpdate);
$result = mysql_query(self::getUpdateQuery());
if(mysql_error())
{
// We add error log here
parent::logError("Update $this->table failed! Database said: " . mysql_error());
} else {
// Execute the after methods
self::executeRelatedMethods($this->afterUpdate);
}
}
/**
* Deletes it's row from the database
*/
public function delete()
{
// We check that beforeDelete() and afterDelete() methods exist
$traceBefore = self::executeRelatedMethods($this->beforeDelete, true);
$traceAfter = self::executeRelatedMethods($this->afterDelete, true);
if($traceBefore == false || $traceAfter == false)
{
parent::logError("The delete hasn't been performed due related methods errors.");
return;
}
// If we got here we will execute the before methods
self::executeRelatedMethods($this->beforeDelete);
$result = mysql_query(self::getDeleteQuery());
if(mysql_error())
{
// We add error log here
parent::logError("Deleting on $this->table failed! Database said: " . mysql_error());
} else {
// We delete dependant models if any
self::cascadingDelete();
// Execute the after methods
self::executeRelatedMethods($this->afterDelete);
}
}
/**
* @Return the insert query for this model
*/
protected function getInsertQuery()
{
// First part of the query
$tempQuery = "INSERT INTO `$this->table` SET ";
// Second part, the fields values stuff
$tempQuery = $tempQuery . self::getFieldsSql(true, false);
return $tempQuery;
}
/**
* @Return the update query for this model
*/
protected function getUpdateQuery()
{
// First part of the query
$tempQuery = "UPDATE `$this->table` SET ";
// Second part, the fields values stuff
$tempQuery = $tempQuery . self::getFieldsSql(false, true);
// Finally, conditions
$tempQuery = $tempQuery . " WHERE id=". $this->id;
return $tempQuery;
}
/**
* @Return the delete query for this model
*/
protected function getDeleteQuery()
{
// First part of the query
$tempQuery = "DELETE FROM `$this->table` WHERE id = '$this->id'";
return $tempQuery;
}
/**
* Creates a part of all Sql statements. field1 = 'value1', field2 = 'value2' and so on
*
* @return <field> = '<value>' string to use in sql queries like save or insert
* It will not do anything with it's own id and table name
* It will trim all values for best database performance
*/
protected function getFieldsSql($insert = false, $update = false)
{
$ignoreFields = array("id");
// If is for insert, we will ignore UPDATE_POPULATED
if($insert == true) { $ignoreFields[] = UPDATE_POPULATED; }
// If is for update, we will ignore DATE_POPULATED AND TIMESTAMP_POPULATED
if($update == true)
{
$ignoreFields[] = DATE_POPULATED;
$ignoreFields[] = TIMESTAMP_POPULATED;
}
// Create sql list of fields and values
foreach($this as $key => $val) {
if(in_array($key, self::getFields()) && !(in_array($key, $ignoreFields)))
{
/*
* When inserting TIMESTAMP_POPULATED we must not put ' ' among the value
* So we'll just have to ask every time
*/
if($key != TIMESTAMP_POPULATED)
{
$returnSql = $returnSql . $key . "='" . trim($val) . "',";
} else {
$returnSql = $returnSql . $key . "=" . trim($val) . ",";
}
}
}
// We return a new string without the last coma
$returnSql = substr($returnSql, 0, strlen($returnSql)-1);
return $returnSql;
}
/**
* Deletes all hasMany and hasOne entries that depends on this one
*/
protected function cascadingDelete()
{
// For relationship possible
foreach($this->possibleRelationships as $relationship)
{
// If this relationship is declared and it's not belongsTo
if(isset($this->$relationship) && $this->relationship != "belongsTo")
{
// Inside it may be lots of relationships of this type
foreach($this->$relationship as $local)
{
// If depends index is declared
if(isset($local['depends']))
{
// If it's value it's true
if($local['depends'] == true)
{
// We request all depending models
$colection = $this->getRelated($local['className']);
// If is not null we delete all of their entries
if($colection != NULL)
{
foreach($colection as $object)
{
$object->delete();
}
}
}
}
}
}
}
}
// END DATABASE QUERIES OPERATION WITH THE MODEL
/*
* DATES AND TIMESTAMP POPULATION FUNCTIONS
*
* They manage the auto-population feature of this framework
*/
/**
* Populates date_modify just before an update action
*/
protected function populateDateModify()
{
if(self::field_exists(UPDATE_POPULATED))
{
// If is not today
$this->date_modify = date("Y-m-d");
}
}
/**
* Populates date_created just before an insert action
*/
public function populateDateCreated()
{
if(self::field_exists(DATE_POPULATED))
{
$this->date_created = date("Y-m-d");
}
}
/**
* Populates timestamp just before an insert action
* (!) It will use mysql constant CURRENT_TIMESTAMP
*/
protected function populateTimeStamp()
{
if(self::field_exists(TIMESTAMP_POPULATED))
{
// If is not today
$this->time_stamp = "CURRENT_TIMESTAMP";
}
}
// END OF AUTO-POPULATING FUNCTIONS
/*
* TABLE'S SCHEMA FUNCTIONS
*
* They stand to provide information about all fields of the model's associated table
* Field Name | Field type | Null | Not_null and so on
* So the model can work in consequence and avoid errors
*/
/**
* Creates an associative array of all fields in our database
* Storing in each index fields name, field type and null/not null indicator
* array['fieldname'] -> array2['type'] = whatever, array2['null'] = bool
*/
public function getSchema()
{
$resource = mysql_list_fields(BBDD, $this->table);
$fields = mysql_num_fields($resource);
for($it=0; $it < $fields; $it++)
{
$schema[mysql_field_name($resource, $it)] = array(
"type" => mysql_field_type($resource, $it),
"null" => $this->isNull(mysql_field_flags($resource, $it))
);
}
return $schema;
}
/**
* Returns wether a field may or not be NULL
*@param string mysql flag
*@return bool
*/
private function isNull($string)
{
//If we find the string not_null then is not null if not is null
$array = explode(" ", $string);
// We look for not_null in those
foreach($array as $portion)
{
if($portion == NOT_NULL_FLAG)
{
return false;
}
}
// If we get here, this field is null
return true;
}
/**
* Gets the list of field names of the table
* @returns array with field names of the table
*/
public function getFields()
{
$resource = mysql_list_fields(BBDD, $this->table);
$fields = mysql_num_fields($resource);
for($it=0; $it < $fields; $it++)
{
$schema[] = mysql_field_name($resource, $it);
}
return $schema;
}
/**
* Returns an array of forbidden fields to be set up manually when creating a new instance
* If forbiddenFields is declared we will add it here =)
* @return array forbidden fields to be set up manually
*/
public function getForbiddenFields()
{
$forbidden = array(
"id",
DATE_POPULATED,
UPDATE_POPULATED,
TIMESTAMP_POPULATED
);
// If ignoredFields is set then we will add them here
if(isset($this->forbiddenFields) && is_array($this->forbiddenFields))
{
$forbidden = array_keys(array_flip(array_merge($forbidden, $this->forbiddenFields)));
}
return $forbidden;
}
/**
* Returns an array of optional fields to be supplied to the constructor
* If ignoredFields is set, they will be added here
* @return array optional fields for the constructor
* Those that are NULL and are NOT forbidden
*/
public function getOptionalFields()
{
// To give back
$optional = array();
// Forbidden fields
$forbidden = self::getForbiddenFields();
// It will return a
$schema = self::getSchema();
// We will add to the optional array all those that are NULL and are NOT in $forbidden
foreach($schema as $field => $info)
{
if($info['null'] == true && in_array($field, $forbidden) == false)
{
$optional[] = $field;
}
}
// If ignoredFields is set then we will add them here
if(isset($this->ignoredFields) && is_array($this->ignoredFields))
{
$optional = array_keys(array_flip(array_merge($optional, $this->ignoredFields)));
}
return $optional;
}
/**
* Returns the bunch of fields required by the constructor to create a new instance
* If $this->ignoredFields is declared, it will not add them to the required fields array
* @return array of required fields to provide to the constructor
* ($All - $optional) - $forbidden
*/
public function getRequiredFields()
{
$all = self::getFields();
$optional = self::getOptionalFields();
$forbidden = self::getForbiddenFields();
$required = array_diff($all, $optional);
$required = array_diff($required, $forbidden);
$required = array_keys(array_flip($required));
// If ignoredFields is set then we remove them from the required array
if(isset($this->ignoredFields) && is_array($this->ignoredFields))
{
$required = array_keys(array_flip(array_diff($required, $this->ignoredFields)));
}
return $required;
}
/**
* Checks in received field exists in our table schema
* @returns boolean
*/
public function field_exists($field)
{
if(in_array($field, self::getFields()))
{
return true;
}
return false;
}
// DATABASE SCHEMA FUNCTIONS END
/*
* VALIDATION FUNCTIONS
* Validate accordding to the validateRules array
* Generate an array of validateErrors()
*/
/**
* Primary validation function
* Check validation for each field declared in array validationRules[]
*/
public function validate()
{
// If no array declared nothing to do
if(!isset($this->validationRules))
return;
/*
* This validator should perform validation set up by the user
* Each $field should satisfy $value
*/
// We need a new instance of validator
$validator = new Validator();
// We'll have to clear up the last validation if any so it can be performed several times
self::clearValidationErrors();
// Proceed
foreach($this->validationRules as $field => $instructions)
{
/*
* $field is the field to validate
* $instructions is an array with two indexes. (method name, throw message)
* 1) Check that method specified works, if not, we should log an error.
* 2) Execute the method, if false, store validateError
*/
/*
* 1) Method exists?
*/
if(!method_exists($validator, $instructions[0]))
{
self::addValidationError("Could not validate field `$field`! The method `$instructions[0]` does not exist in class ` " . get_class($validator) . "`");
} else {
/*
* 2) Executin' baby yeah
* Loading validator error if fails
*/
if($validator->$instructions[0]($this->$field) == false)
{
self::addValidationError($instructions[1]);
}
}
}
// Cleaning up trash
unset($validator);
}
/**
* Adds an error to validationErrors[]
* @param string error
*/
public function addValidationError($string)
{
$this->validationErrors[] = $string;
}
/**
* Clears up validationErrors
* @param string error
*/
private function clearValidationErrors()
{
$this->validationErrors = NULL;
}
/**
* Returns wether validation is Ok or not
* @return bool
*/
public function validationOk()
{
if($this->validationErrors != NULL)
{
return false;
} else {
return true;
}
}
/**
* Returns wether validation failed or not
* @return bool
*/
public function validationFailed()
{
if($this->validationErrors == NULL)
{
return false;
} else {
return true;
}
}
/**
* @return array of validation errors
* Usefull to pass it to dataErrors in the view and display it
*/
public function getValidationErrors()
{
return $this->validationErrors;
}
/**
* Displays on screen all validation errors
* Debug purposes only
*/
public function displayValidationErrors()
{
if(self::getValidationErrors() != NULL)
{
echo "<b>VALIDATION ERRORS</b><ul>";
foreach($this->validationErrors as $error)
{
echo "<li>$error</li>";
}
echo "</ul>";
} else {
echo "<b>NO VALIDATION ERRORS</b>";
}
}
/*
* MODEL RELATIONSHIPS
* Functions that allow the model to return related models if asked
* Given a model, it will look for it in the ['className'] index of all possibleRelantionships
*/
/**
* @param modelname String
* @return foreignKey of the relationship. Null if not declared
*/
public function getRelationshipFK($requestedModel)
{
// First, we iterate all possible relationships
foreach($this->possibleRelationships as $relationship)
{
// If this relationship is declared
if(isset($this->$relationship))
{
// Inside it may be lots of relationships of this type
foreach($this->$relationship as $local)
{
// If this relationship relates the requested model
if($local['className'] == $requestedModel)
{
// Return the foreignKey
return $local['foreignKey'];
}
}
}
}
// If got here...it is not declared =)
return NULL;
}
/**
* Gives requested related models
* @return array of requested models related to our model or NULL
* @param Model Requested
* @param boolean justids
* @param boolean justNumRows
* @param boolean rowcount
* @param boolean offset
*/
public function getRelated($requestedModel,
$justids = false,
$justNumRows = false,
$offset = NULL,
$rowcount = NULL)
{
// First, we iterate all possible relationships
foreach($this->possibleRelationships as $relationship)
{
// If this relationship is declared
if(isset($this->$relationship))
{
// Inside it may be lots of relationships of this type
foreach($this->$relationship as $local)
{
// If this relationship relates the requested model
if($local['className'] == $requestedModel)
{
// We attemp to load it
$try = Core::loadModel($local['className']);
// If failed loading the model we're out.
if($try == false)
return NULL;
// If foreignKey is empty, we are out
if(Validator::isEmpty($local['foreignKey']))
return NULL;
/*
* Query
* 1) If relationship is belongsTo, the foreignKey value is in our object
* 2) If not, the foreign key and the value should be queried to the related model
*/
/*
* belongsTo relationship
*/
if($relationship == "belongsTo")
{
// If the foreignKey does not exist in our object, return null.
if(!self::field_exists($local['foreignKey']))
return null;
// If exist...we just create the object or get the id and return it
if($justids == false)
{
$related[] = new $requestedModel($this->$local['foreignKey']);
} else {
$related[] = $this->$local['foreignKey'];
}
return $related;
// (!) No integrity check for belongsTo
// So the user may know if some foreignKey is broken
} else {
/*
* Other relationShip
* We have to query the related models table with foreignkey = our object key
*/
// First we have to check that specified foreignKey exists on the related model
$test = new $requestedModel(0);
if(!$test->field_exists($local['foreignKey']))
return NULL;
// Everything cool. Query and return
$related = Query::searchBy($local['foreignKey'], $this->id, $requestedModel, false, $justids, $offset, $rowcount);
// Will be NULL if no results
return $related;
}
}
}
}
}
// If out here hasn't been any return performed
return NULL;
}
/**
* MODEL SEARCH-ENGINE RELATED METHODS
*/
/**
* @return if this model is searchable
*/
public function isSearchable()
{
if(isset($this->searchFields))
return true;
return false;
}
/*
* MISC STUFF
*/
/**
* It will refresh all of it's values
* Useful when having usually changing data
* (!) Used by the constructor
* @param id
*/
public function refresh()
{
// Our Id
$id = $this->id;
// Our query
$result = mysql_query("SELECT * FROM `$this->table` WHERE id = '$id' ");
// If no result
if(mysql_num_rows($result) < 1)
{
parent::logError("There is no row in table $this->table with ID = $id");
} else {
// Temp object
$temp = mysql_fetch_object($result);
// Self-asignement
foreach($temp as $key => $value)
{
$this->$key = $value;
}
}
}
/**
* Displays up front a clear representation of the model's fields and values
* This should be used for debugging/developing only
*/
public function examine()
{
foreach(self::getFields() as $key) {
echo("<b>" . $key . "</b><br> " . $this->$key . "<br>");
}
}
/**
* SCAFFOLDING STUFF
* Provides information about scaffolding...foreign keys etc, etc xD
*/
/**
* @return which fields of the table schema might be foreign keys to be populated when scaffolding
*/
public function getForeignFields()
{
// Fields
$fields = array();
// We get the field list
$schema = self::getFields();
// Check if any follows the TABLENAME_id pattern. If it does, we append it to an array
foreach($schema as $field)
{
// Segments
$matches = explode("_", $field);
// Amount
$amount = count($matches) -1;
if($amount > 0)
{
// Check if last part is 'id'...then it's a foreign field!
if($matches[$amount] == "id")
{
// Now let's check if the model exists. Must be the first part of the name, capitalized.
for($it = 0; $it < count($matches) -1; $it++)
{
$modelname = $modelname . $matches[$it];
}
$modelname = ucfirst($modelname);
if(Core::modelExists($modelname))
$fields[] = $field;
}
}
}
return $fields;
}
}
?>