├── .gitignore ├── database ├── test.sqlite3 └── another_test.sqlite3 ├── models ├── user_namespaced.php ├── another_user.php ├── account.php ├── bill.php ├── ticket.php └── user.php ├── .travis.yml ├── test ├── factories │ ├── account.php │ ├── ticket.php │ └── user.php ├── torm.yml ├── run ├── logTest.php ├── inflectionsTest.php ├── factoryTest.php └── cacheTest.php ├── drivers ├── sqlite.php ├── mysql.php ├── postgresql.php └── oracle.php ├── composer.json ├── src ├── Util.php ├── Storage.php ├── Driver.php ├── Dirty.php ├── HasOne.php ├── Scopes.php ├── Builder.php ├── Validations.php ├── Inflections.php ├── Log.php ├── Errors.php ├── BelongsTo.php ├── Finders.php ├── Validation.php ├── Callbacks.php ├── Connection.php ├── Factory.php ├── Cache.php ├── Sequences.php ├── HasMany.php ├── Persistence.php ├── Collection.php └── Model.php ├── torm.php ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | vendor/* 3 | composer.lock 4 | -------------------------------------------------------------------------------- /database/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taq/torm/HEAD/database/test.sqlite3 -------------------------------------------------------------------------------- /database/another_test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taq/torm/HEAD/database/another_test.sqlite3 -------------------------------------------------------------------------------- /models/user_namespaced.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.4' 4 | - '5.5' 5 | - '5.6' 6 | - '7.0' 7 | script: ./test/run 8 | -------------------------------------------------------------------------------- /models/another_user.php: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /test/factories/account.php: -------------------------------------------------------------------------------- 1 | 1,"account_number"=>"12345")); 3 | ?> 4 | -------------------------------------------------------------------------------- /test/factories/ticket.php: -------------------------------------------------------------------------------- 1 | 1,"description"=>"Just another ticket")); 3 | ?> 4 | -------------------------------------------------------------------------------- /models/account.php: -------------------------------------------------------------------------------- 1 | true)); 4 | ?> 5 | -------------------------------------------------------------------------------- /drivers/sqlite.php: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /drivers/mysql.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /drivers/postgresql.php: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test/torm.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en-US: 3 | errors: 4 | messages: 5 | presence: "cannot be blank" 6 | format: "has invalid format" 7 | uniqueness: "must be unique" 8 | numericality: "must be a number" 9 | models: 10 | user: "User" 11 | attributes: 12 | user: 13 | name: "Name" 14 | email: "E-mail" 15 | user_level: "Level" 16 | -------------------------------------------------------------------------------- /models/bill.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | 14 | /** 15 | * Bill model 16 | * 17 | * PHP version 5.5 18 | * 19 | * @category Tests 20 | * @package TORM 21 | * @author Eustáquio Rangel 22 | * @license http://www.gnu.org/copyleft/gpl.html GPL 23 | * @link http://github.com/taq/torm 24 | */ 25 | class Bill extends TORM\Model 26 | { 27 | } 28 | 29 | Bill::setOrder("id"); 30 | ?> 31 | -------------------------------------------------------------------------------- /drivers/oracle.php: -------------------------------------------------------------------------------- 1 | = %from%"; 9 | Driver::$pagination_subquery = true; 10 | Driver::$numeric_column = "number"; 11 | Driver::$current_timestamp = "sysdate"; 12 | Driver::$escape_char = ""; 13 | Driver::$last_id_supported = false; 14 | ?> 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taq/torm", 3 | "type": "library", 4 | "description": "An ORM based on ActiveRecord", 5 | "keywords": ["database","orm","activerecord"], 6 | "homepage": "http://github.com/taq/torm", 7 | "license": "GPL-2.0-or-later", 8 | "version": "1.3.4", 9 | "authors": [ 10 | { 11 | "name": "Eustaquio Rangel", 12 | "email": "eustaquiorangel@gmail.com", 13 | "homepage": "http://eustaquiorangel.com", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.3.0", 19 | "taq/pdooci": "^1.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "TORM\\": "src" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export TORM_ENV=test 3 | test=$1 4 | filter=$2 5 | 6 | for file in $(find -maxdepth 1 -iname '*.php' -type f) 7 | do 8 | if [ -n "$test" -a "$file" != "./$test" ]; then 9 | continue 10 | fi 11 | 12 | echo "running $file ..." 13 | if [ -n "$filter" ]; then 14 | phpunit --colors --debug --filter "$filter" $file 15 | else 16 | phpunit --colors --debug $file 17 | fi 18 | 19 | rst=$? 20 | echo "test result: $rst errors" 21 | 22 | if [ "$rst" != "0" ]; then 23 | echo "*****************************************************************"; 24 | echo "Failed!" 25 | echo "*****************************************************************"; 26 | exit 1 27 | fi 28 | done 29 | -------------------------------------------------------------------------------- /models/ticket.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | 14 | /** 15 | * Ticket model 16 | * 17 | * PHP version 5.5 18 | * 19 | * @category Tests 20 | * @package TORM 21 | * @author Eustáquio Rangel 22 | * @license http://www.gnu.org/copyleft/gpl.html GPL 23 | * @link http://github.com/taq/torm 24 | */ 25 | class Ticket extends TORM\Model 26 | { 27 | /** 28 | * Return new primary key value 29 | * 30 | * @return int value 31 | */ 32 | public static function getNewPKValue() 33 | { 34 | return time() + mt_rand(1, 10000); 35 | } 36 | } 37 | 38 | Ticket::validates("description", array("presence" => true)); 39 | Ticket::belongsTo("user"); 40 | Ticket::setOrder("id"); 41 | ?> 42 | -------------------------------------------------------------------------------- /src/Util.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Model 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Util 27 | { 28 | /** 29 | * Decamelize a string 30 | * 31 | * @param string $str string 32 | * 33 | * @return string decamelized 34 | */ 35 | public static function decamelize($str) 36 | { 37 | return substr(preg_replace_callback('/([A-Z][a-z]+)/', 38 | function($matches) { 39 | return strtolower($matches[0])."_"; 40 | }, $str), 0, -1); 41 | } 42 | } 43 | ?> 44 | -------------------------------------------------------------------------------- /torm.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | 14 | // traits 15 | require_once "src/Finders.php"; 16 | require_once "src/Storage.php"; 17 | require_once "src/Persistence.php"; 18 | require_once "src/Errors.php"; 19 | require_once "src/Validations.php"; 20 | require_once "src/Scopes.php"; 21 | require_once "src/HasMany.php"; 22 | require_once "src/HasOne.php"; 23 | require_once "src/BelongsTo.php"; 24 | require_once "src/Sequences.php"; 25 | require_once "src/Cache.php"; 26 | require_once "src/Callbacks.php"; 27 | require_once "src/Dirty.php"; 28 | 29 | // classes 30 | require_once "src/Connection.php"; 31 | require_once "src/Builder.php"; 32 | require_once "src/Model.php"; 33 | require_once "src/Collection.php"; 34 | require_once "src/Factory.php"; 35 | require_once "src/Driver.php"; 36 | require_once "src/Validation.php"; 37 | require_once "src/Log.php"; 38 | require_once "src/Inflections.php"; 39 | ?> 40 | -------------------------------------------------------------------------------- /src/Storage.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Storage 16 | { 17 | private $_data = array(); 18 | private $_prev_data = array(); 19 | private $_orig_data = array(); 20 | 21 | /** 22 | * Set data 23 | * 24 | * @param mixed $data data 25 | * 26 | * @return null 27 | */ 28 | private function _setData($data) 29 | { 30 | $this->_data = $data; 31 | } 32 | 33 | /** 34 | * Return the object current values 35 | * 36 | * @return mixed data 37 | */ 38 | public function getData() 39 | { 40 | return $this->_data; 41 | } 42 | 43 | /** 44 | * Return the previous data array 45 | * 46 | * @return mixed data 47 | */ 48 | public function getPrevData() 49 | { 50 | return $this->_prev_data; 51 | } 52 | 53 | /** 54 | * Return the original data array, immutable through the life of the object 55 | * 56 | * @return mixed data 57 | */ 58 | public function getOriginalData() 59 | { 60 | return $this->_orig_data ; 61 | } 62 | } 63 | ?> 64 | -------------------------------------------------------------------------------- /src/Driver.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Driver main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Driver 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Driver 27 | { 28 | const PRIMARY_KEY_DELETE = 1; 29 | const PRIMARY_KEY_STRING = 2; 30 | const PRIMARY_KEY_EXECUTE = 3; 31 | const PRIMARY_KEY_EVAL = 4; 32 | const PRIMARY_KEY_SEQUENCE = 5; 33 | 34 | const LIMIT_APPEND = 11; 35 | const LIMIT_AROUND = 12; 36 | 37 | public static $name = null; 38 | public static $primary_key_behaviour = self::PRIMARY_KEY_DELETE; 39 | public static $limit_behaviour = self::LIMIT_APPEND; 40 | public static $limit_query = null; 41 | public static $numeric_column = "integer"; 42 | public static $escape_char = "\""; 43 | public static $current_timestamp = "current_timestamp"; 44 | public static $pagination_query = "%query% limit %to% offset %from%"; 45 | public static $pagination_subquery = false; 46 | public static $last_id_supported = true; 47 | } 48 | -------------------------------------------------------------------------------- /test/factories/user.php: -------------------------------------------------------------------------------- 1 | time(), 4 | "name" => "Mary Doe", 5 | "email" => "mary@doe.com", 6 | "user_level" => 1, 7 | "code" => "12345", 8 | "created_at" => null, 9 | "updated_at" => null)); 10 | 11 | TORM\Factory::define("admin",array( 12 | "id" => time(), 13 | "name" => "Mary Doe", 14 | "email" => "mary@doe.com", 15 | "user_level" => 1, 16 | "code" => "12345", 17 | "created_at" => null, 18 | "updated_at" => null), 19 | array("class_name"=>"User")); 20 | 21 | TORM\Factory::define("unnamed_user",array( 22 | "id" => time(), 23 | "name" => null, 24 | "email" => "mary@doe.com", 25 | "user_level" => 1, 26 | "code" => "12345", 27 | "created_at" => null, 28 | "updated_at" => null), 29 | array("class_name"=>"User")); 30 | 31 | TORM\Factory::define("crazy_user",array( 32 | "id" => time(), 33 | "name" => "Mary Doe", 34 | "email" => "mary@doe.com", 35 | "user_level" => 1, 36 | "code" => "12345", 37 | "invalid_attr" => "invalid", 38 | "created_at" => null, 39 | "updated_at" => null), 40 | array("class_name"=>"User")); 41 | ?> 42 | -------------------------------------------------------------------------------- /test/logTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | require_once "../vendor/autoload.php"; 14 | 15 | /** 16 | * Main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Tests 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class LogTest extends PHPUnit\Framework\TestCase 27 | { 28 | protected static $log_file = '/tmp/torm.log'; 29 | 30 | /** 31 | * Run before each test 32 | * 33 | * @return null 34 | */ 35 | public function setUp() 36 | { 37 | TORM\Log::enable(true); 38 | } 39 | 40 | /** 41 | * Run after each test 42 | * 43 | * @return null 44 | */ 45 | public function tearDown() 46 | { 47 | TORM\Log::enable(false); 48 | } 49 | 50 | /** 51 | * Delete log file 52 | * 53 | * @param string $file if null, open default file 54 | * 55 | * @return null 56 | */ 57 | private function _deleteLog($file = null) 58 | { 59 | $file = is_null($file) ? self::$log_file : $file; 60 | 61 | if (file_exists($file)) { 62 | unlink($file); 63 | } 64 | } 65 | 66 | /** 67 | * Test echo 68 | * 69 | * @return null 70 | */ 71 | public function testEcho() 72 | { 73 | $this->_deleteLog(); 74 | TORM\Log::log('hello'); 75 | $this->assertFalse(file_exists(self::$log_file)); 76 | } 77 | 78 | /** 79 | * Test file 80 | * 81 | * @return null 82 | */ 83 | public function testFile() 84 | { 85 | $custom = "/tmp/torm-test.log"; 86 | $this->_deleteLog($custom); 87 | 88 | TORM\Log::file($custom); 89 | TORM\Log::log('hello'); 90 | TORM\Log::log('world'); 91 | 92 | $this->assertTrue(file_exists($custom)); 93 | $this->assertEquals("hello\nworld\n", file_get_contents($custom)); 94 | $this->_deleteLog($custom); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Dirty.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Dirty 16 | { 17 | /** 18 | * Check if an attribute changed its value since object was loaded 19 | * 20 | * @param string $attr attribute 21 | * 22 | * @return changed or not 23 | */ 24 | private function _changedAttribute($attr) 25 | { 26 | preg_match('/(\w+)_(change|changed|was)$/', $attr, $matches); 27 | if (sizeof($matches) < 1) { 28 | return null; 29 | } 30 | 31 | $attr = $matches[1]; 32 | $meth = $matches[2]; 33 | $cur = $this->get($attr); 34 | $old = $this->get($attr, false); 35 | 36 | if ($meth == "was") { 37 | return $old; 38 | } 39 | if ($meth == "changed") { 40 | return $cur!=$old; 41 | } 42 | if ($meth == "change") { 43 | return array($old, $cur); 44 | } 45 | return null; 46 | } 47 | 48 | /** 49 | * Return what was changed, as column as key and old and current values 50 | * 51 | * @return changes 52 | */ 53 | public function changes() 54 | { 55 | return $this->changed(true); 56 | } 57 | 58 | /** 59 | * Return what changed 60 | * 61 | * @param boolean $attrs if true, return old and current values 62 | * 63 | * @return changes 64 | */ 65 | public function changed($attrs=false) 66 | { 67 | $changes = array(); 68 | $cls = get_called_class(); 69 | 70 | foreach (self::$_columns[$cls] as $column) { 71 | // if is the primary key or one of the automatic columns, skip 72 | if ($cls::getPK() == $column || in_array(strtolower($column), array("created_at","updated_at"))) { 73 | continue; 74 | } 75 | 76 | $cur = $this->get($column); // current value 77 | $old = $this->get($column, false); // old value 78 | 79 | // if same value, didn't change 80 | if ($cur == $old) { 81 | continue; 82 | } 83 | 84 | // set the value as the changed column name 85 | $value = $column; 86 | 87 | // if checking the attributes, return an array with both values 88 | if ($attrs) { 89 | $value = array($old, $cur); 90 | $changes[$column] = $value; 91 | } else { 92 | array_push($changes, $value); 93 | } 94 | } 95 | return $changes; 96 | } 97 | } 98 | ?> 99 | -------------------------------------------------------------------------------- /src/HasOne.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait HasOne 16 | { 17 | private static $_has_one = array(); 18 | private $_has_one_cache = array(); 19 | 20 | /** 21 | * Create a has one association 22 | * 23 | * @param string $attr attribute 24 | * @param mixed $options options to use 25 | * 26 | * @return null 27 | */ 28 | public static function hasOne($attr, $options=null) 29 | { 30 | $cls = get_called_class(); 31 | if (!array_key_exists($cls, self::$_has_one)) { 32 | self::$_has_one[$cls] = array(); 33 | } 34 | self::$_has_one[$cls][$attr] = $options ? $options : false; 35 | } 36 | 37 | /** 38 | * Resolve the has one association and returns the object 39 | * 40 | * @param string $attr name 41 | * @param mixed $value to use 42 | * 43 | * @return collection 44 | */ 45 | private function _resolveHasOne($attr, $value) 46 | { 47 | $cls = get_called_class(); 48 | if (!array_key_exists($cls, self::$_has_one) 49 | || !array_key_exists($attr, self::$_has_one[$cls]) 50 | ) { 51 | return null; 52 | } 53 | 54 | if (array_key_exists($attr, $this->_has_one_cache) && $this->_has_one_cache[$attr]) { 55 | return $this->_has_one_cache[$attr]; 56 | } 57 | 58 | $configs = self::$_has_one[$cls][$attr]; 59 | $has_one_cls = is_array($configs) && array_key_exists("class_name", $configs) ? $configs["class_name"] : ucfirst(preg_replace('/s$/', "", $attr)); 60 | $this_key = is_array($configs) && array_key_exists("foreign_key", $configs) ? $configs["foreign_key"] : (self::isIgnoringCase() ? strtolower($cls)."_id" : $cls."_id"); 61 | $obj = $has_one_cls::first(array($this_key=>$value)); 62 | 63 | if ($obj) { 64 | $this->_has_one_cache[$attr] = $obj; 65 | } 66 | return $obj; 67 | } 68 | 69 | /** 70 | * Check and return the value of a has one association 71 | * 72 | * @param string $method searched 73 | * @param mixed $value the current id 74 | * 75 | * @return association 76 | */ 77 | private function _checkAndReturnHasOne($method, $value) 78 | { 79 | $cls = get_called_class(); 80 | if (array_key_exists($cls, self::$_has_one) 81 | && array_key_exists($method, self::$_has_one[$cls]) 82 | ) { 83 | return $this->_resolveHasOne($method, $value); 84 | } 85 | } 86 | } 87 | ?> 88 | -------------------------------------------------------------------------------- /src/Scopes.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Scopes 16 | { 17 | private static $_scopes = array(); 18 | 19 | /** 20 | * Create a scope 21 | * 22 | * @param string $name of scope 23 | * @param mixed $conditions to avail 24 | * 25 | * @return null 26 | */ 27 | public static function scope($name, $conditions) 28 | { 29 | $cls = get_called_class(); 30 | 31 | if (!array_key_exists($cls, self::$_scopes)) { 32 | self::$_scopes[$cls] = array(); 33 | } 34 | 35 | if (!array_key_exists($name, self::$_scopes[$cls])) { 36 | self::$_scopes[$cls][$name] = array(); 37 | } 38 | 39 | self::$_scopes[$cls][$name] = $conditions; 40 | } 41 | 42 | /** 43 | * Return a scope 44 | * 45 | * @param string $name of scope 46 | * @param string $cls class of scope 47 | * 48 | * @return mixed scope 49 | */ 50 | public static function getScope($name, $cls = null) 51 | { 52 | if (!$cls) { 53 | $cls = get_called_class(); 54 | } 55 | 56 | if (!array_key_exists($cls, self::$_scopes) 57 | || !array_key_exists($name, self::$_scopes[$cls]) 58 | ) { 59 | return null; 60 | } 61 | 62 | return self::$_scopes[$cls][$name]; 63 | } 64 | 65 | /** 66 | * Resolve a scope, returning a collection 67 | * 68 | * @param string $name of scope 69 | * @param mixed $args arguments to use on callable scopes 70 | * @param string $cls class of scope 71 | * 72 | * @return mixed collection 73 | */ 74 | public static function resolveScope($name, $args=null, $cls=null) 75 | { 76 | $conditions = self::getScope($name, $cls); 77 | if (!$conditions) { 78 | return null; 79 | } 80 | 81 | if (is_callable($conditions)) { 82 | $conditions = $conditions($args); 83 | } 84 | return self::where($conditions); 85 | } 86 | 87 | /** 88 | * Call static methods as scopes 89 | * 90 | * @param string $method method 91 | * @param mixed $args arguments 92 | * 93 | * @return scope result or null 94 | */ 95 | public static function __callStatic($method, $args) 96 | { 97 | $scope = self::resolveScope($method, $args); 98 | if ($scope) { 99 | return $scope; 100 | } 101 | return null; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/inflectionsTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | require_once "../vendor/autoload.php"; 14 | require_once "../models/user.php"; 15 | require_once "../models/ticket.php"; 16 | require_once "../models/account.php"; 17 | 18 | /** 19 | * Main class 20 | * 21 | * PHP version 5.5 22 | * 23 | * @category Tests 24 | * @package TORM 25 | * @author Eustáquio Rangel 26 | * @license http://www.gnu.org/copyleft/gpl.html GPL 27 | * @link http://github.com/taq/torm 28 | */ 29 | class InflectionsTest extends PHPUnit\Framework\TestCase 30 | { 31 | /** 32 | * Run before each test 33 | * 34 | * @return null 35 | */ 36 | public function setUp() 37 | { 38 | TORM\Inflections::push(TORM\Inflections::IRREGULAR, "person", "people"); 39 | TORM\Inflections::push(TORM\Inflections::SINGULAR, '/ao$/i', "oes"); 40 | TORM\Inflections::push(TORM\Inflections::PLURAL, '/oes$/i', "ao"); 41 | 42 | error_reporting(E_ALL); 43 | } 44 | 45 | /** 46 | * Test pluralization 47 | * 48 | * @return null 49 | */ 50 | public function testPluralize() 51 | { 52 | $this->assertEquals("people", TORM\Inflections::pluralize("person")); 53 | } 54 | 55 | /** 56 | * Test singularization 57 | * 58 | * @return null 59 | */ 60 | public function testSingularize() 61 | { 62 | $this->assertEquals("person", TORM\Inflections::singularize("people")); 63 | } 64 | 65 | /** 66 | * Test default pluralization 67 | * 68 | * @return null 69 | */ 70 | public function testDefaultPluralize() 71 | { 72 | $this->assertEquals("books", TORM\Inflections::pluralize("book")); 73 | } 74 | 75 | /** 76 | * Test default singularization 77 | * 78 | * @return null 79 | */ 80 | public function testDefaultSingularize() 81 | { 82 | $this->assertEquals("book", TORM\Inflections::singularize("books")); 83 | } 84 | 85 | /** 86 | * Test pluralization with regex 87 | * 88 | * @return null 89 | */ 90 | public function testPluralizeWithRegex() 91 | { 92 | $this->assertEquals("acoes", TORM\Inflections::pluralize("acao")); 93 | } 94 | 95 | /** 96 | * Test singularization with regex 97 | * 98 | * @return null 99 | */ 100 | public function testSingularizeWithRegex() 101 | { 102 | $this->assertEquals("acao", TORM\Inflections::singularize("acoes")); 103 | } 104 | } 105 | ?> 106 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | 11 | * @license http://www.gnu.org/copyleft/gpl.html GPL 12 | * @link http://github.com/taq/torm 13 | */ 14 | namespace TORM; 15 | 16 | /** 17 | * Builder class 18 | * Main class 19 | * 20 | * PHP version 5.5 21 | * 22 | * @category Associations 23 | * @package TORM 24 | * @author Eustáquio Rangel 25 | * @license http://www.gnu.org/copyleft/gpl.html GPL 26 | * @link http://github.com/taq/torm 27 | */ 28 | class Builder 29 | { 30 | public $prefix = "select "; 31 | public $fields = null; 32 | public $table = null; 33 | public $where = null; 34 | public $limit = null; 35 | public $offset = null; 36 | public $order = null; 37 | 38 | /** 39 | * Convert to string 40 | * 41 | * @return SQL query string 42 | */ 43 | public function toString() 44 | { 45 | $array = array(); 46 | $escape = Driver::$escape_char; 47 | 48 | array_push($array, $this->prefix." "); 49 | 50 | if (is_null($this->fields)) { 51 | array_push($array, $escape.$this->table.$escape.".* "); 52 | } else { 53 | array_push($array, $this->fields); 54 | } 55 | 56 | array_push($array, " from $escape".$this->table."$escape "); 57 | 58 | if ($this->where) { 59 | array_push($array, " where ".$this->where); 60 | } 61 | 62 | if ($this->order) { 63 | array_push($array, " order by ".$this->order); 64 | } 65 | 66 | if (!is_null($this->limit) && is_null($this->offset) && Driver::$limit_behaviour == Driver::LIMIT_APPEND) { 67 | array_push($array, " limit ".$this->limit); 68 | } 69 | 70 | // basic query 71 | $query = join(" ", $array); 72 | 73 | if (!is_null($this->limit) && is_null($this->offset) 74 | && Driver::$limit_behaviour==Driver::LIMIT_AROUND 75 | && Driver::$limit_query 76 | ) { 77 | $query = str_replace("%query%", $query, Driver::$limit_query); 78 | $query = str_replace("%limit%", $this->limit, $query); 79 | } 80 | 81 | if (!is_null($this->limit) && !is_null($this->offset) && Driver::$pagination_query) { 82 | $query = str_replace("%query%", $query, Driver::$pagination_query); 83 | $query = str_replace("%from%", $this->offset, $query); 84 | $query = str_replace("%to%", $this->limit, $query); 85 | } 86 | return $query; 87 | } 88 | 89 | /** 90 | * PHP method to convert to string 91 | * 92 | * @return SQL query string 93 | */ 94 | public function __toString() 95 | { 96 | return $this->toString(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Validations.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Validations 16 | { 17 | private static $_validations = array(); 18 | 19 | /** 20 | * Check if object is valid 21 | * 22 | * @return boolean valid? 23 | */ 24 | public function isValid() 25 | { 26 | $this->_resetErrors(); 27 | $cls = get_called_class(); 28 | $rtn = true; 29 | $pk = self::get(self::getPK()); 30 | 31 | if (!array_key_exists($cls, self::$_validations) 32 | || sizeof(self::$_validations[$cls]) < 1 33 | ) { 34 | return true; 35 | } 36 | 37 | foreach (self::$_validations[$cls] as $attr => $validations) { 38 | $value = $this->_data[$attr]; 39 | 40 | foreach ($validations as $validation) { 41 | $validation_key = array_keys($validation); 42 | $validation_key = $validation_key[0]; 43 | $validation_value = array_values($validation); 44 | $validation_value = $validation_value[0]; 45 | 46 | $args = array(get_called_class(), $pk, $attr, $value, $validation_value, $validation); 47 | $test = call_user_func_array(array("TORM\Validation", $validation_key), $args); 48 | 49 | if (!$test) { 50 | $rtn = false; 51 | $this->_addError($attr, Validation::$validation_map[$validation_key]); 52 | } 53 | } 54 | } 55 | return $rtn; 56 | } 57 | 58 | /** 59 | * Check if attribute is unique 60 | * 61 | * @param mixed $id id 62 | * @param mixed $attr to check 63 | * @param mixed $attr_value to check 64 | * 65 | * @return if attribute is unique 66 | */ 67 | public static function isUnique($id, $attr, $attr_value) 68 | { 69 | $obj = self::first(array($attr => $attr_value)); 70 | return $obj==null || $obj->get(self::getPK()) == $id; 71 | } 72 | 73 | /** 74 | * Validates an attribute with a validation rule 75 | * 76 | * @param string $attr attribute 77 | * @param mixed $validation validation rule 78 | * 79 | * @return valid or not 80 | */ 81 | public static function validates($attr, $validation) 82 | { 83 | $cls = get_called_class(); 84 | 85 | if (!array_key_exists($cls, self::$_validations)) { 86 | self::$_validations[$cls] = array(); 87 | } 88 | 89 | if (!array_key_exists($attr, self::$_validations[$cls])) { 90 | self::$_validations[$cls][$attr] = array(); 91 | } 92 | 93 | array_push(self::$_validations[$cls][$attr], $validation); 94 | } 95 | } 96 | ?> 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TORM 2 | 3 | | :warning: **WARNING** This software is [abandonware](https://en.wikipedia.org/wiki/Abandonware). As the creator and maintainer, I don't even use PHP _for years_, so, I can't support it anymore. It should work ok for PHP untill version 7, but seems that with 8.1 there are some alerts. Feel free to fork it and keep it going. | 4 | | --- | 5 | 6 | Just another simple ORM for PHP. You can use it, but don't ask why I made it. Right? :-) 7 | 8 | ## Usage 9 | 10 | Take a look on the [Github Wiki](https://github.com/taq/torm/wiki) for documentation, but let me show the basics here: 11 | 12 | ```php 13 | true]); 25 | User::validates("email", ["presence" => true]); 26 | User::validates("email", ["uniqueness" => true]); 27 | User::validates("id", ["numericality" => true]); 28 | 29 | // create some relations 30 | User::hasMany("tickets"); 31 | User::hasOne("account"); 32 | Ticket::belongsTo("user"); 33 | 34 | // this will create a new user 35 | $user = new User(); 36 | $user->name = "John Doe"; 37 | $user->email = "john@doe.com"; 38 | $user->level = 1; 39 | $user->save(); 40 | 41 | // this will find the user using its primary key 42 | $user = User::find(1); 43 | 44 | // find some users 45 | $users = User::where(["level" => 1]); 46 | 47 | // find some users, using more complex expressions 48 | // the array first element is the query, the rest are values 49 | $users = User::where(["level >= ?", 1]); 50 | 51 | // updating users 52 | User::where(["level" => 1])->updateAttributes(["level" => 3]); 53 | 54 | // using fluent queries 55 | $users = User::where(["level" => 1])->limit(5)->order("name desc"); 56 | 57 | // listing the user tickets 58 | foreach($user->tickets as $ticket) { 59 | echo $ticket->description; 60 | } 61 | 62 | // show user account info 63 | echo $user->account->number; 64 | ?> 65 | ``` 66 | 67 | ## Testing 68 | 69 | ### SQLite 70 | 71 | First, use `composer update` to make sure everything is ok with all the 72 | packages. Then, go to the `test` directory and run `run`. It will requires the 73 | [SQLite driver](http://php.net/manual/en/ref.pdo-sqlite.php) so make sure it is 74 | available. If not, check the `php.ini` dir found with 75 | 76 | ```bash 77 | $ php -r 'phpinfo();' | grep 'php.ini' 78 | Configuration File (php.ini) Path => /etc/php/7.1/cli 79 | Loaded Configuration File => /etc/php/7.1/cli/php.ini 80 | ``` 81 | 82 | and, if not found there or on the `conf.d` on the same location the `php.ini` 83 | file is, it can be installed, on Ubuntu, using: 84 | 85 | ```bash 86 | $ sudo apt install php-sqlite3 87 | ``` 88 | 89 | ### Multibyte strings, locale and YAML 90 | 91 | ``` 92 | $ sudo apt install php-mbstring php-intl php-yaml 93 | ``` 94 | -------------------------------------------------------------------------------- /models/user.php: -------------------------------------------------------------------------------- 1 | email = str_replace("#","",$this->email); 11 | return true; 12 | } 13 | 14 | public function after_save() { 15 | //echo "user after save callback!\n"; 16 | file_put_contents("/tmp/torm-after-save.log","torm test"); 17 | return true; 18 | } 19 | 20 | public function before_create() 21 | { 22 | file_put_contents("/tmp/torm-before-create.log", "torm test"); 23 | return true; 24 | } 25 | 26 | public function after_create() 27 | { 28 | file_put_contents("/tmp/torm-after-create.log", "torm test"); 29 | return true; 30 | } 31 | 32 | public function before_update() 33 | { 34 | file_put_contents("/tmp/torm-before-update.log", "torm test"); 35 | return true; 36 | } 37 | 38 | public function after_update() 39 | { 40 | file_put_contents("/tmp/torm-after-update.log", "torm test"); 41 | return true; 42 | } 43 | 44 | public function before_destroy() { 45 | //echo "user before destroy callback!\n"; 46 | file_put_contents("/tmp/torm-before-destroy.log","torm test"); 47 | return true; 48 | } 49 | 50 | public function after_destroy() { 51 | //echo "user after destroy callback!\n"; 52 | file_put_contents("/tmp/torm-after-destroy.log","torm test"); 53 | return true; 54 | } 55 | 56 | public function afterInitialize() { 57 | if (is_null($this->name)) { 58 | $this->name = "Unnamed User"; 59 | } 60 | } 61 | } 62 | 63 | User::setOrder("name"); 64 | User::validates("name" ,array("presence"=>true)); 65 | User::validates("name" ,array("format"=>"^[\p{L},]{2,} [\p{L}\s\.]{2,}")); 66 | User::validates("email",array("presence"=>true)); 67 | User::validates("email",array("format" =>"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$")); 68 | User::validates("email",array("uniqueness"=>true,"allow_null"=>true,"allow_blank"=>true)); 69 | User::validates("user_level",array("numericality"=>true)); 70 | User::validates("code" ,array("format"=>"^[0-9]{5}$","allow_null"=>true)); 71 | 72 | User::hasMany("tickets",array("class_name"=>"Ticket")); 73 | User::hasOne("account"); 74 | 75 | User::beforeSave("before_save"); 76 | User::beforeSave("strip_invalid"); 77 | User::afterSave("after_save"); 78 | 79 | User::beforeDestroy("before_destroy"); 80 | User::afterDestroy("after_destroy"); 81 | 82 | User::beforeCreate("before_create"); 83 | User::afterCreate("after_create"); 84 | 85 | User::beforeUpdate("before_update"); 86 | User::afterUpdate("after_update"); 87 | 88 | User::scope("first_level",array("user_level"=>1)); 89 | User::scope("by_level",function($args) { return "user_level=".$args[0]; }); 90 | User::scope("by_level_and_date",function($args) { return "user_level=".$args[0]." and created_at<'".$args[1]." 23:59:59'"; }); 91 | User::scope("doe", "email like '%doe.com'"); 92 | User::scope("email_first", function($args) { return "email like '".$args[0]."%'"; }); 93 | ?> 94 | -------------------------------------------------------------------------------- /src/Inflections.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Inflection main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Inflections 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Inflections 27 | { 28 | const SINGULAR = 0; 29 | const PLURAL = 1; 30 | const IRREGULAR = 2; 31 | 32 | private static $_inflections = array(); 33 | 34 | /** 35 | * Push an inflection 36 | * 37 | * @param mixed $idx index 38 | * @param string $singular form 39 | * @param string $plural form 40 | * 41 | * @return null 42 | */ 43 | public static function push($idx, $singular, $plural) 44 | { 45 | self::_initialize(); 46 | self::$_inflections[$idx][$singular] = $plural; 47 | } 48 | 49 | /** 50 | * Initialize inflections 51 | * 52 | * @return null 53 | */ 54 | private static function _initialize() 55 | { 56 | for ($i=self::SINGULAR; $i <= self::IRREGULAR; $i++) { 57 | if (!array_key_exists($i, self::$_inflections)) { 58 | self::$_inflections[$i] = array(); 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * Pluralize 65 | * 66 | * @param string $str string 67 | * 68 | * @return pluralized string 69 | */ 70 | public static function pluralize($str) 71 | { 72 | return self::_search($str, self::PLURAL); 73 | } 74 | 75 | /** 76 | * Singularize 77 | * 78 | * @param string $str string 79 | * 80 | * @return singlarized string 81 | */ 82 | public static function singularize($str) 83 | { 84 | return self::_search($str, self::SINGULAR); 85 | } 86 | 87 | /** 88 | * Search an inflection 89 | * 90 | * @param string $str string 91 | * @param mixed $idx index 92 | * 93 | * @return inflection 94 | */ 95 | private static function _search($str,$idx) 96 | { 97 | self::_initialize(); 98 | 99 | $idx = $idx == self::PLURAL ? self::SINGULAR : self::PLURAL; 100 | $vals = self::$_inflections[$idx]; 101 | 102 | // adding irregular 103 | foreach (self::$_inflections[self::IRREGULAR] as $key => $val) { 104 | $vals[$key] = $val; 105 | $vals[$val] = $key; 106 | } 107 | 108 | foreach ($vals as $key => $val) { 109 | $reg = preg_match('/^\/[\s\S]+\/[imsxeADSUXJu]?$/', $key); 110 | $exp = $reg ? $key : "/$key/i"; 111 | $mat = preg_match($exp, $str); 112 | 113 | if (!$reg && $mat) { 114 | return $val; 115 | } 116 | if ($reg && $mat) { 117 | return preg_replace($key, $val, $str); 118 | } 119 | } 120 | 121 | // default behaviour - the "s" thing 122 | return $idx == self::SINGULAR ? trim($str)."s" : preg_replace('/s$/', "", $str); 123 | } 124 | } 125 | ?> 126 | -------------------------------------------------------------------------------- /src/Log.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Log main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Log 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Log 27 | { 28 | private static $_enabled = false; 29 | private static $_file = null; 30 | 31 | /** 32 | * Return if log is enabled 33 | * 34 | * @return enabled or not 35 | */ 36 | public static function isEnabled() 37 | { 38 | return self::$_enabled; 39 | } 40 | 41 | /** 42 | * Enable or disable log 43 | * 44 | * @param boolean $status enable (true) or disabled (false) 45 | * 46 | * @return null 47 | */ 48 | public static function enable($status) 49 | { 50 | self::$_enabled = $status; 51 | if ($status) { 52 | register_shutdown_function(array('TORM\Log', 'destruct')); 53 | } 54 | } 55 | 56 | /** 57 | * Enable or disable file writing 58 | * 59 | * @param string $file or null 60 | * 61 | * @return null 62 | */ 63 | public static function file($file = null) 64 | { 65 | self::$_file = $file; 66 | } 67 | 68 | /** 69 | * Send a message to log 70 | * 71 | * @param string $msg message 72 | * 73 | * @return null 74 | */ 75 | public static function log($msg) 76 | { 77 | if (!self::$_enabled) { 78 | return; 79 | } 80 | 81 | if (is_null(self::$_file)) { 82 | echo "log: $msg\n"; 83 | return; 84 | } 85 | 86 | return self::_logToFile($msg); 87 | } 88 | 89 | /** 90 | * Write a message on file 91 | * 92 | * @param string $msg message 93 | * 94 | * @return true or false 95 | */ 96 | private static function _logToFile($msg) 97 | { 98 | if (is_null(self::$_file) || (is_string(self::$_file) && !self::_openLogFile())) { 99 | return false; 100 | } 101 | return fwrite(self::$_file, "$msg\n"); 102 | } 103 | 104 | /** 105 | * Open log file 106 | * 107 | * @return boolean 108 | */ 109 | private static function _openLogFile() 110 | { 111 | try { 112 | self::$_file = fopen(self::$_file, "a"); 113 | 114 | if (!self::$_file) { 115 | self::$_file = null; 116 | return false; 117 | } 118 | } catch (Exception $e) { 119 | } 120 | return true; 121 | } 122 | 123 | /** 124 | * Destructor 125 | * 126 | * Try to close the log file, if is there one 127 | * 128 | * @return true if there were an open file opened, false if not 129 | */ 130 | public static function destruct() 131 | { 132 | if (is_null(self::$_file) || !is_resource(self::$_file)) { 133 | return false; 134 | } 135 | 136 | try { 137 | fclose(self::$_file); 138 | } catch (Exception $e) { 139 | return false; 140 | } 141 | return true; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test/factoryTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | require_once "../vendor/autoload.php"; 14 | require_once "../models/user.php"; 15 | 16 | /** 17 | * Main class 18 | * 19 | * PHP version 5.5 20 | * 21 | * @category Tests 22 | * @package TORM 23 | * @author Eustáquio Rangel 24 | * @license http://www.gnu.org/copyleft/gpl.html GPL 25 | * @link http://github.com/taq/torm 26 | */ 27 | class FactoryTest extends PHPUnit\Framework\TestCase 28 | { 29 | protected static $con = null; 30 | 31 | /** 32 | * Run before each test 33 | * 34 | * @return null 35 | */ 36 | public static function setUpBeforeClass() 37 | { 38 | $file = realpath(dirname(__FILE__)."/../database/test.sqlite3"); 39 | self::$con = new PDO("sqlite:$file"); 40 | 41 | TORM\Connection::setConnection(self::$con, "test"); 42 | TORM\Connection::setDriver("sqlite"); 43 | TORM\Factory::setFactoriesPath("./factories"); 44 | TORM\Log::enable(false); 45 | 46 | error_reporting(E_ALL); 47 | } 48 | 49 | /** 50 | * Test getting factories 51 | * 52 | * @return null 53 | */ 54 | public function testGetFactories() 55 | { 56 | $this->assertEquals(6, TORM\Factory::factoriesCount()); 57 | } 58 | 59 | /** 60 | * Test getting a specific factory 61 | * 62 | * @return null 63 | */ 64 | public function testGetFactory() 65 | { 66 | $this->assertNotNull(TORM\Factory::get("user")); 67 | } 68 | 69 | /** 70 | * Test a factory with different class 71 | * 72 | * @return null 73 | */ 74 | public function testFactoryWithDifferentClass() 75 | { 76 | $admin = TORM\Factory::build("admin"); 77 | $this->assertNotNull($admin); 78 | $this->assertEquals("User", get_class($admin)); 79 | } 80 | 81 | /** 82 | * Test building a factory 83 | * 84 | * @return null 85 | */ 86 | public function testBuildFactory() 87 | { 88 | $user = TORM\Factory::build("user"); 89 | $this->assertEquals("User", get_class($user)); 90 | $this->assertEquals("Mary Doe", $user->name); 91 | $this->assertEquals("mary@doe.com", $user->email); 92 | } 93 | 94 | /** 95 | * Test factory attributes 96 | * 97 | * @return null 98 | */ 99 | public function testAttributes() 100 | { 101 | $data = TORM\Factory::attributes_for("user"); 102 | $this->assertNotNull($data); 103 | $this->assertTrue(is_array($data)); 104 | $this->assertEquals("Mary Doe", $data["name"]); 105 | $this->assertEquals("mary@doe.com", $data["email"]); 106 | } 107 | 108 | /** 109 | * Test factory creation 110 | * 111 | * @return null 112 | */ 113 | public function testCreateFactory() 114 | { 115 | $user = TORM\Factory::create("user"); 116 | $this->assertEquals("User", get_class($user)); 117 | $this->assertNotNull($user->id); 118 | $this->assertEquals("Mary Doe", $user->name); 119 | $this->assertEquals("mary@doe.com", $user->email); 120 | $this->assertNotNull(User::find($user->id)); 121 | $this->assertTrue($user->destroy()); 122 | } 123 | } 124 | ?> 125 | -------------------------------------------------------------------------------- /src/Errors.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Errors 16 | { 17 | /** 18 | * Add an error to an attribute 19 | * 20 | * @param string $attr attribute 21 | * @param string $msg message 22 | * 23 | * @return null 24 | */ 25 | public function addError($attr, $msg) 26 | { 27 | $this->_addError($attr, $msg); 28 | } 29 | 30 | /** 31 | * Add an error to an attribute 32 | * 33 | * @param string $attr attribute 34 | * @param string $msg message 35 | * 36 | * @return null 37 | */ 38 | private function _addError($attr, $msg) 39 | { 40 | if (!array_key_exists($attr, $this->errors)) { 41 | $this->errors[$attr] = array(); 42 | } 43 | array_push($this->errors[$attr], $msg); 44 | } 45 | 46 | /** 47 | * Return the error messages 48 | * 49 | * @return mixed error messages 50 | */ 51 | public function errorMessages() 52 | { 53 | $msgs = array(); 54 | foreach ($this->errors as $key => $values) { 55 | foreach ($values as $value) { 56 | array_push($msgs, "$key $value"); 57 | } 58 | } 59 | return $msgs; 60 | } 61 | 62 | /** 63 | * Sets the YAML file location 64 | * 65 | * @param string $file location 66 | * 67 | * @return null 68 | */ 69 | public static function setYAMLFile($file) 70 | { 71 | self::$yaml_file = $file; 72 | } 73 | 74 | /** 75 | * Return textual and translated error messages 76 | * 77 | * @param mixed $errors found 78 | * 79 | * @return mixed error messages 80 | */ 81 | public function fullMessages($errors=null) 82 | { 83 | if (!function_exists("yaml_parse") 84 | || is_null(self::$yaml_file) 85 | || !file_exists(self::$yaml_file) 86 | ) { 87 | return array(); 88 | } 89 | 90 | $rtn = array(); 91 | $parsed = yaml_parse(file_get_contents(self::$yaml_file)); 92 | $errors = is_null($errors) ? $this->errors : $errors; 93 | $locale = function_exists("locale_get_default") ? locale_get_default() : "en-US"; 94 | 95 | if (!array_key_exists($locale, $parsed) 96 | || !array_key_exists("errors", $parsed[$locale]) 97 | || !array_key_exists("messages", $parsed[$locale]["errors"]) 98 | ) { 99 | return $this->errorMessages(); 100 | } 101 | 102 | $msgs = $parsed[$locale]["errors"]["messages"]; 103 | $cls = strtolower(get_called_class()); 104 | 105 | foreach ($errors as $key => $values) { 106 | $attr = array_key_exists("attributes", $parsed[$locale]) && 107 | array_key_exists($cls, $parsed[$locale]["attributes"]) && 108 | array_key_exists($key, $parsed[$locale]["attributes"][$cls]) ? 109 | $parsed[$locale]["attributes"][$cls][$key] : $key; 110 | 111 | foreach ($values as $value) { 112 | $msg = array_key_exists($value, $msgs) ? $msgs[$value] : ":$value"; 113 | array_push($rtn, "$attr $msg"); 114 | } 115 | } 116 | return $rtn; 117 | } 118 | 119 | /** 120 | * Reset errors 121 | * 122 | * @return null 123 | */ 124 | private function _resetErrors() 125 | { 126 | $this->errors = array(); 127 | } 128 | } 129 | ?> 130 | -------------------------------------------------------------------------------- /src/BelongsTo.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait BelongsTo 16 | { 17 | private static $_belongs_to = array(); 18 | private $_belongs_cache = array(); 19 | 20 | /** 21 | * Create a belongs relationship 22 | * 23 | * @param string $model model 24 | * @param mixed $options options for relation 25 | * 26 | * @return null 27 | */ 28 | public static function belongsTo($model, $options=null) 29 | { 30 | $cls = get_called_class(); 31 | if (!array_key_exists($cls, self::$_belongs_to)) { 32 | self::$_belongs_to[$cls] = array(); 33 | } 34 | self::$_belongs_to[$cls][$model] = $options ? $options : false; 35 | } 36 | 37 | /** 38 | * Check if is there a belongs to association and return it 39 | * 40 | * @param string $attr attribute 41 | * @param mixed $values values to use 42 | * 43 | * @return mixed association 44 | */ 45 | private function _checkAndReturnBelongs($attr, $values) 46 | { 47 | $cls = get_called_class(); 48 | if (array_key_exists($cls, self::$_belongs_to) 49 | && array_key_exists($attr, self::$_belongs_to[$cls]) 50 | ) { 51 | return $this->_resolveBelongsTo($attr, $values); 52 | } 53 | } 54 | 55 | /** 56 | * Resolve a belongs to association 57 | * 58 | * @param string $attr attribute 59 | * @param mixed $values values to use 60 | * 61 | * @return association 62 | */ 63 | private function _resolveBelongsTo($attr, $values) 64 | { 65 | $cls = get_called_class(); 66 | if (!array_key_exists($cls, self::$_belongs_to) 67 | || !array_key_exists($attr, self::$_belongs_to[$cls]) 68 | ) { 69 | return null; 70 | } 71 | 72 | $configs = self::$_belongs_to[$cls][$attr]; 73 | $belongs_cls = is_array($configs) && array_key_exists("class_name", $configs) ? $configs["class_name"] : ucfirst($attr); 74 | $belongs_key = is_array($configs) && array_key_exists("foreign_key", $configs) ? $configs["foreign_key"] : strtolower($belongs_cls)."_id"; 75 | $primary_key = is_array($configs) && array_key_exists("primary_key", $configs) ? $configs["primary_key"] : "id"; 76 | $value = $values[$belongs_key]; 77 | 78 | if (array_key_exists($attr, $this->_belongs_cache) && $this->_belongs_cache[$attr] && $this->_belongs_cache[$attr]->$primary_key == $value) { 79 | return $this->_belongs_cache[$attr]; 80 | } 81 | 82 | $obj = $belongs_cls::first(array($primary_key => $value)); 83 | 84 | if ($obj) { 85 | $this->_belongs_cache[$attr] = $obj; 86 | } 87 | return $obj; 88 | } 89 | 90 | /** 91 | * Check the belongs key key/attribute 92 | * 93 | * @param string $attr attribute 94 | * 95 | * @return key found or null 96 | */ 97 | private function _getBelongsKey($attr) 98 | { 99 | $cls = get_called_class(); 100 | 101 | if (!array_key_exists($cls, self::$_belongs_to) 102 | || !array_key_exists($attr, self::$_belongs_to[$cls]) 103 | ) { 104 | return null; 105 | } 106 | $configs = self::$_belongs_to[$cls][$attr]; 107 | $belongs_cls = is_array($configs) && array_key_exists("class_name", $configs) ? $configs["class_name"] : ucfirst($attr); 108 | $belongs_key = is_array($configs) && array_key_exists("foreign_key", $configs) ? $configs["foreign_key"] : strtolower($belongs_cls)."_id"; 109 | return $belongs_key; 110 | } 111 | } 112 | ?> 113 | -------------------------------------------------------------------------------- /test/cacheTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | require_once "../vendor/autoload.php"; 14 | 15 | /** 16 | * Cache test main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Tests 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class CacheTest extends PHPUnit\Framework\TestCase 27 | { 28 | protected static $con = null; 29 | protected static $user = null; 30 | 31 | /** 32 | * Run when initializing 33 | * 34 | * @return null 35 | */ 36 | public static function setUpBeforeClass() 37 | { 38 | $file = realpath(dirname(__FILE__)."/../database/test.sqlite3"); 39 | self::$con = new PDO("sqlite:$file"); 40 | // self::$con = new PDO('mysql:host=localhost;dbname=torm',"torm","torm"); 41 | 42 | TORM\Connection::setConnection(self::$con, "test"); 43 | TORM\Connection::setEncoding("UTF-8"); 44 | TORM\Connection::setDriver("sqlite"); 45 | TORM\Factory::setFactoriesPath("./factories"); 46 | TORM\Log::enable(false); 47 | TORM\Cache::getInstance()->setTimeout(300); 48 | 49 | error_reporting(E_ALL); 50 | } 51 | 52 | /** 53 | * Test cache method 54 | * 55 | * @return null 56 | */ 57 | public function testCache() 58 | { 59 | TORM\Cache::getInstance()->clear(); 60 | $sql = "select * from Users where id=?"; 61 | $this->assertNull(TORM\Cache::getInstance()->get($sql)); 62 | TORM\Cache::getInstance()->put($sql); 63 | $this->assertNotNull(TORM\Cache::getInstance()->get($sql)); 64 | } 65 | 66 | /** 67 | * Test cache timeout 68 | * 69 | * @return null 70 | */ 71 | public function testCacheTimeout() 72 | { 73 | TORM\Cache::getInstance()->clear(); 74 | TORM\Cache::getInstance()->setTimeout(1); 75 | 76 | $sql = "select * from Users where id=?"; 77 | $this->assertNull(TORM\Cache::getInstance()->get($sql)); 78 | TORM\Cache::getInstance()->put($sql); 79 | $size = TORM\Cache::getInstance()->size() > 0; 80 | $this->assertTrue($size > 0); 81 | sleep(2); 82 | $this->assertNull(TORM\Cache::getInstance()->get($sql)); 83 | $this->assertEquals($size - 1, TORM\Cache::getInstance()->size()); 84 | } 85 | 86 | /** 87 | * Test cache expiration function 88 | * 89 | * @return null 90 | */ 91 | public function testCacheExpiration() 92 | { 93 | TORM\Cache::getInstance()->setTimeout(3); 94 | TORM\Cache::getInstance()->clear(); 95 | 96 | $this->assertFalse(TORM\Cache::getInstance()->expireCache()); 97 | sleep(1); 98 | $this->assertFalse(TORM\Cache::getInstance()->expireCache()); 99 | sleep(2); 100 | $this->assertTrue(TORM\Cache::getInstance()->expireCache()); 101 | sleep(1); 102 | $this->assertFalse(TORM\Cache::getInstance()->expireCache()); 103 | } 104 | 105 | /** 106 | * Test cache size 107 | * 108 | * @return null 109 | */ 110 | public function testCacheSize() 111 | { 112 | $sql = "select * from Users where id=?"; 113 | TORM\Cache::getInstance()->put($sql); 114 | $this->assertTrue(TORM\Cache::getInstance()->size() > 0); 115 | } 116 | 117 | /** 118 | * Test clear cache 119 | * 120 | * @return null 121 | */ 122 | public function testClearCache() 123 | { 124 | $sql = "select * from Users where id=?"; 125 | TORM\Cache::getInstance()->put($sql); 126 | $this->assertTrue(TORM\Cache::getInstance()->size() > 0); 127 | TORM\Cache::getInstance()->clear(); 128 | $this->assertEquals(0, TORM\Cache::getInstance()->size()); 129 | } 130 | } 131 | ?> 132 | -------------------------------------------------------------------------------- /src/Finders.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Finders 16 | { 17 | /** 18 | * Find an object by its primary key 19 | * 20 | * @param object $id primary key 21 | * 22 | * @return mixed result 23 | */ 24 | public static function find($id) 25 | { 26 | self::_checkLoaded(); 27 | 28 | $pk = self::isIgnoringCase() ? strtolower(self::getPK()) : self::getPK(); 29 | $builder = self::_makeBuilder(); 30 | $builder->fields = self::extractColumns(); 31 | $builder->where = self::_extractWhereConditions(array($pk=>$id)); 32 | $builder->limit = 1; 33 | 34 | $cls = get_called_class(); 35 | $stmt = self::executePrepared($builder, array($id)); 36 | $data = $stmt->fetch(\PDO::FETCH_ASSOC); 37 | 38 | if (!$data) { 39 | return null; 40 | } 41 | return new $cls($data); 42 | } 43 | 44 | /** 45 | * Use the WHERE clause to return values 46 | * 47 | * @param mixed $conditions string or array - better use is using an array 48 | * 49 | * @return Collection of results 50 | */ 51 | public static function where($conditions) 52 | { 53 | self::_checkLoaded(); 54 | 55 | $builder = self::_makeBuilder(); 56 | $builder->where = self::_extractWhereConditions($conditions); 57 | $builder->fields = self::extractColumns(); 58 | $vals = self::extractWhereValues($conditions); 59 | return new Collection($builder, $vals, get_called_class()); 60 | } 61 | 62 | /** 63 | * Return all values (from an optional condition) 64 | * 65 | * @param mixed $conditions optional 66 | * 67 | * @return Collection values 68 | */ 69 | public static function all($conditions=null) 70 | { 71 | self::_checkLoaded(); 72 | 73 | $builder = self::_makeBuilder(); 74 | $builder->fields = self::extractColumns(); 75 | $vals = null; 76 | 77 | if ($conditions) { 78 | $builder->where = self::_extractWhereConditions($conditions); 79 | $vals = self::extractWhereValues($conditions); 80 | } 81 | return new Collection($builder, $vals, get_called_class()); 82 | } 83 | 84 | /** 85 | * Get result by position - first or last 86 | * 87 | * @param string $position "first" or "last" 88 | * @param mixed $conditions to extract 89 | * 90 | * @return result or null 91 | */ 92 | private static function _getByPosition($position, $conditions=null) 93 | { 94 | self::_checkLoaded(); 95 | 96 | $builder = self::_makeBuilder(); 97 | $builder->fields = self::extractColumns(); 98 | $builder->order = $position=="first" ? self::getOrder() : self::getReversedOrder(); 99 | $builder->where = self::_extractWhereConditions($conditions); 100 | $vals = self::extractWhereValues($conditions); 101 | 102 | $cls = get_called_class(); 103 | $stmt = self::executePrepared($builder, $vals); 104 | $data = $stmt->fetch(\PDO::FETCH_ASSOC); 105 | 106 | if (!$data) { 107 | return null; 108 | } 109 | return new $cls($data); 110 | } 111 | 112 | /** 113 | * Return the first value (get by order) 114 | * 115 | * @param mixed $conditions to return 116 | * 117 | * @return mixed result 118 | */ 119 | public static function first($conditions=null) 120 | { 121 | return self::_getByPosition("first", $conditions); 122 | } 123 | 124 | /** 125 | * Return the last value (get by inverse order) 126 | * 127 | * @param mixed $conditions to return 128 | * 129 | * @return object result 130 | */ 131 | public static function last($conditions=null) 132 | { 133 | return self::_getByPosition("last", $conditions); 134 | } 135 | 136 | } 137 | ?> 138 | -------------------------------------------------------------------------------- /src/Validation.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Validation main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Validations 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Validation 27 | { 28 | const VALIDATION_PRESENCE = "presence"; 29 | const VALIDATION_FORMAT = "format"; 30 | const VALIDATION_UNIQUENESS = "uniqueness"; 31 | const VALIDATION_NUMERICALITY = "numericality"; 32 | 33 | public static $validation_map = array( 34 | "presence" => self::VALIDATION_PRESENCE, 35 | "format" => self::VALIDATION_FORMAT, 36 | "uniqueness" => self::VALIDATION_UNIQUENESS, 37 | "numericality" => self::VALIDATION_NUMERICALITY 38 | ); 39 | 40 | /** 41 | * Check if an attribute is present 42 | * 43 | * @param string $cls class 44 | * @param mixed $id id 45 | * @param string $attr attribute 46 | * @param mixed $attr_value attribute value 47 | * @param mixed $validation_value validation value 48 | * @param mixed $options options 49 | * 50 | * @return valid or not 51 | */ 52 | public static function presence($cls, $id, $attr, $attr_value, $validation_value, $options) 53 | { 54 | if (!$validation_value) { 55 | return true; 56 | } 57 | return strlen(trim($attr_value)) > 0; 58 | } 59 | 60 | /** 61 | * Check if an attribute format is ok 62 | * 63 | * @param string $cls class 64 | * @param mixed $id id 65 | * @param string $attr attribute 66 | * @param mixed $attr_value attribute value 67 | * @param mixed $validation_value validation value 68 | * @param mixed $options options 69 | * 70 | * @return valid or not 71 | */ 72 | public static function format($cls, $id, $attr, $attr_value, $validation_value, $options) 73 | { 74 | // check if allow blank values 75 | if (!is_null($options) && array_key_exists("allow_blank", $options) && strlen(trim($attr_value)) < 1) { 76 | return true; 77 | } 78 | 79 | // check if allow null values 80 | if (!is_null($options) && array_key_exists("allow_null", $options) && is_null($attr_value)) { 81 | return true; 82 | } 83 | 84 | // check format using regex 85 | return preg_match("/$validation_value/u", $attr_value); 86 | } 87 | 88 | /** 89 | * Check if an attribute is unique 90 | * 91 | * @param string $cls class 92 | * @param mixed $id id 93 | * @param string $attr attribute 94 | * @param mixed $attr_value attribute value 95 | * @param mixed $validation_value validation value 96 | * @param mixed $options options 97 | * 98 | * @return valid or not 99 | */ 100 | public static function uniqueness($cls, $id, $attr, $attr_value, $validation_value, $options) 101 | { 102 | // check if allow null values 103 | if (!is_null($options) && array_key_exists("allow_null", $options) && is_null($attr_value)) { 104 | return true; 105 | } 106 | // check if allow blank values 107 | if (!is_null($options) && array_key_exists("allow_blank", $options) && strlen(trim($attr_value)) < 1) { 108 | return true; 109 | } 110 | // use a class method to check if it is unique 111 | return call_user_func_array(array("\\".$cls, "isUnique"), array($id, $attr, $attr_value)); 112 | } 113 | 114 | /** 115 | * Check if an attribute is numeric 116 | * 117 | * @param string $cls class 118 | * @param mixed $id id 119 | * @param string $attr attribute 120 | * @param mixed $attr_value attribute value 121 | * @param mixed $validation_value validation value 122 | * @param mixed $options options 123 | * 124 | * @return valid or not 125 | */ 126 | public static function numericality($cls, $id, $attr, $attr_value, $validation_value, $options) 127 | { 128 | return preg_match("/^[-\.0-9]+$/", trim($attr_value)); 129 | } 130 | } 131 | ?> 132 | -------------------------------------------------------------------------------- /src/Callbacks.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Callbacks 16 | { 17 | private static $_callbacks = array(); 18 | 19 | /** 20 | * Before save callback 21 | * 22 | * @param mixed $func callback 23 | * 24 | * @return null 25 | */ 26 | public static function beforeSave($func) 27 | { 28 | $cls = get_called_class(); 29 | self::_addCallback($cls, "before_save", $func); 30 | } 31 | 32 | /** 33 | * Before save callback 34 | * 35 | * @param mixed $func callback 36 | * 37 | * @return null 38 | */ 39 | public static function afterSave($func) 40 | { 41 | $cls = get_called_class(); 42 | self::_addCallback($cls, "after_save", $func); 43 | } 44 | 45 | /** 46 | * Before save callback 47 | * 48 | * @param mixed $func callback 49 | * 50 | * @return null 51 | */ 52 | public static function beforeDestroy($func) 53 | { 54 | $cls = get_called_class(); 55 | self::_addCallback($cls, "before_destroy", $func); 56 | } 57 | 58 | /** 59 | * Before save callback 60 | * 61 | * @param mixed $func callback 62 | * 63 | * @return null 64 | */ 65 | public static function afterDestroy($func) 66 | { 67 | $cls = get_called_class(); 68 | self::_addCallback($cls, "after_destroy", $func); 69 | } 70 | 71 | /** 72 | * Before create 73 | * 74 | * @param mixed $func callback 75 | * 76 | * @return null 77 | */ 78 | public static function beforeCreate($func) 79 | { 80 | $cls = get_called_class(); 81 | self::_addCallback($cls, "before_create", $func); 82 | } 83 | 84 | /** 85 | * After create 86 | * 87 | * @param mixed $func callback 88 | * 89 | * @return null 90 | */ 91 | public static function afterCreate($func) 92 | { 93 | $cls = get_called_class(); 94 | self::_addCallback($cls, "after_create", $func); 95 | } 96 | 97 | /** 98 | * Before update 99 | * 100 | * @param mixed $func callback 101 | * 102 | * @return null 103 | */ 104 | public static function beforeUpdate($func) 105 | { 106 | $cls = get_called_class(); 107 | self::_addCallback($cls, "before_update", $func); 108 | } 109 | 110 | /** 111 | * After update 112 | * 113 | * @param mixed $func callback 114 | * 115 | * @return null 116 | */ 117 | public static function afterUpdate($func) 118 | { 119 | $cls = get_called_class(); 120 | self::_addCallback($cls, "after_update", $func); 121 | } 122 | 123 | /** 124 | * Check if a callback exists 125 | * 126 | * @param string $cls class 127 | * @param string $callback callback 128 | * @param mixed $context context (instance) 129 | * 130 | * @return exist or not 131 | */ 132 | private static function _checkCallback($cls, $callback, $context) 133 | { 134 | self::_initiateCallbacks($cls); 135 | foreach (self::$_callbacks[$cls][$callback] as $func) { 136 | if (!call_user_func(array($context, $func))) { 137 | return false; 138 | } 139 | } 140 | return true; 141 | } 142 | 143 | /** 144 | * Add callback 145 | * 146 | * @param string $cls class 147 | * @param mixed $callback method 148 | * @param mixed $func to call 149 | * 150 | * @return null 151 | */ 152 | private function _addCallback($cls, $callback, $func) 153 | { 154 | self::_initiateCallbacks($cls); 155 | array_push(self::$_callbacks[$cls][$callback], $func); 156 | } 157 | 158 | /** 159 | * Initiate callbacks 160 | * 161 | * @param string $cls class 162 | * 163 | * @return null 164 | */ 165 | private static function _initiateCallbacks($cls) 166 | { 167 | if (!array_key_exists($cls, self::$_callbacks)) { 168 | self::$_callbacks[$cls] = array(); 169 | } 170 | 171 | $callbacks = array( 172 | "before_save", 173 | "after_save", 174 | "before_destroy", 175 | "after_destroy", 176 | "before_create", 177 | "after_create", 178 | "before_update", 179 | "after_update" 180 | ); 181 | 182 | foreach ($callbacks as $callback) { 183 | if (!array_key_exists($callback, self::$_callbacks[$cls])) { 184 | self::$_callbacks[$cls][$callback] = array(); 185 | } 186 | } 187 | } 188 | } 189 | ?> 190 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Connection main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Connection 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Connection 27 | { 28 | private static $_driver = array( 29 | "development"=> "sqlite", 30 | "test" => "sqlite", 31 | "production" => "sqlite"); 32 | 33 | private static $_connection = array( 34 | "development"=> null, 35 | "test" => null, 36 | "production" => null); 37 | 38 | private static $_encoding = null; 39 | 40 | /** 41 | * Set the connection handle 42 | * 43 | * @param mixed $con connection handle 44 | * @param string $env environment 45 | * 46 | * @return null 47 | */ 48 | public static function setConnection($con, $env = null) 49 | { 50 | $env = self::selectEnvironment($env); 51 | self::$_connection[$env] = $con; 52 | 53 | // just send an exception when not on production mode 54 | if (in_array($env, array("development","test"))) { 55 | self::setErrorHandling(\PDO::ERRMODE_EXCEPTION); 56 | } 57 | } 58 | 59 | /** 60 | * Return the connection handle 61 | * 62 | * @param string $env environment 63 | * 64 | * @return mixed connection handle 65 | */ 66 | public static function getConnection($env = null) 67 | { 68 | return self::$_connection[self::selectEnvironment($env)]; 69 | } 70 | 71 | /** 72 | * Return the current environment 73 | * 74 | * @param string $env enviroment to be forced 75 | * 76 | * @return string environment 77 | */ 78 | public static function selectEnvironment($env = null) 79 | { 80 | if (strlen($env) < 1) { 81 | $getenv = self::_getEnvironment(); 82 | if (strlen($getenv) > 0) { 83 | $env = $getenv; 84 | } else { 85 | $env = "development"; 86 | } 87 | } 88 | return $env; 89 | } 90 | 91 | /** 92 | * Return environment from the environment variable 93 | * 94 | * @return string environment 95 | */ 96 | private static function _getEnvironment() 97 | { 98 | return getenv("TORM_ENV"); 99 | } 100 | 101 | /** 102 | * Set the connection database driver 103 | * 104 | * @param string $driver database driver 105 | * @param string $env environment 106 | * 107 | * @return driver file 108 | */ 109 | public static function setDriver($driver, $env = null) 110 | { 111 | $file = realpath(dirname(__FILE__)."/../drivers/$driver.php"); 112 | if (!file_exists($file)) { 113 | Log::log("ERROR: Driver file $file does not exists"); 114 | return null; 115 | } 116 | self::$_driver[self::selectEnvironment($env)] = $driver; 117 | include_once $file; 118 | } 119 | 120 | /** 121 | * Return the database driver 122 | * 123 | * @param string $env environment to check 124 | * 125 | * @return string driver 126 | */ 127 | public static function getDriver($env = null) 128 | { 129 | return self::$_driver[self::selectEnvironment($env)]; 130 | } 131 | 132 | /** 133 | * Set the error handling strategy 134 | * 135 | * @param mixed $strategy strategy 136 | * 137 | * @return null 138 | */ 139 | public static function setErrorHandling($strategy) 140 | { 141 | self::getConnection()->setAttribute(\PDO::ATTR_ERRMODE, $strategy); 142 | } 143 | 144 | /** 145 | * Set encoding 146 | * 147 | * @param string $encoding encoding 148 | * 149 | * @return null 150 | */ 151 | public static function setEncoding($encoding) 152 | { 153 | self::$_encoding = $encoding; 154 | } 155 | 156 | /** 157 | * Return encoding 158 | * 159 | * @return string encoding 160 | */ 161 | public static function getEncoding() 162 | { 163 | return self::$_encoding; 164 | } 165 | 166 | /** 167 | * Convert a string to the specified encoding 168 | * Paranoid checking 169 | * 170 | * @param string $mixed string to encode 171 | * 172 | * @return string encoded 173 | */ 174 | public static function convertToEncoding($mixed) 175 | { 176 | if (is_null(self::$_encoding) 177 | || is_numeric($mixed) 178 | || is_bool($mixed) 179 | || is_object($mixed) 180 | || is_array($mixed) 181 | || !is_string($mixed) 182 | || !function_exists('mb_convert_encoding') 183 | ) { 184 | return $mixed; 185 | } 186 | return \mb_convert_encoding($mixed, self::$_encoding); 187 | } 188 | } 189 | ?> 190 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Factory main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Factories 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Factory 27 | { 28 | private static $_path = null; 29 | private static $_factories = array(); 30 | private static $_options = array(); 31 | private static $_loaded = false; 32 | 33 | /** 34 | * Set the factories path 35 | * 36 | * @param string $path path 37 | * 38 | * @return null 39 | */ 40 | public static function setFactoriesPath($path) 41 | { 42 | self::$_path = $path; 43 | } 44 | 45 | /** 46 | * Return the factories path 47 | * 48 | * @return factories path 49 | */ 50 | public static function getFactoriesPath() 51 | { 52 | self::_resolveDefaultPath(); 53 | return self::$_path; 54 | } 55 | 56 | /** 57 | * Resolve the default factory path 58 | * 59 | * @return null 60 | */ 61 | private static function _resolveDefaultPath() 62 | { 63 | if (!self::$_path) { 64 | self::$_path = realpath(dirname(__FILE__)."/factories"); 65 | } 66 | } 67 | 68 | /** 69 | * Return the factories count 70 | * 71 | * @return int count 72 | */ 73 | public static function factoriesCount() 74 | { 75 | self::load(); 76 | return count(self::$_factories); 77 | } 78 | 79 | /** 80 | * Define a factory 81 | * 82 | * @param string $name factory name 83 | * @param mixed $attrs attributes 84 | * @param mixed $options options to use 85 | * 86 | * @return null 87 | */ 88 | public static function define($name, $attrs, $options = null) 89 | { 90 | self::$_factories[$name] = $attrs; 91 | self::$_options[$name] = $options; 92 | } 93 | 94 | /** 95 | * Return a factory 96 | * 97 | * @param string $name factory name 98 | * 99 | * @return mixed factory 100 | */ 101 | public static function get($name) 102 | { 103 | self::load(); 104 | if (!array_key_exists($name, self::$_factories)) { 105 | return null; 106 | } 107 | return self::$_factories[$name]; 108 | } 109 | 110 | /** 111 | * Load factories 112 | * 113 | * @param boolean $force force loading 114 | * 115 | * @return boolean loaded or not 116 | */ 117 | public static function load($force = false) 118 | { 119 | // already loaded 120 | if (!$force && self::$_loaded) { 121 | return false; 122 | } 123 | self::_resolveDefaultPath(); 124 | 125 | $files = glob(realpath(self::$_path)."/*.php"); 126 | foreach ($files as $file) { 127 | Log::log("loading factory from $file ..."); 128 | include_once $file; 129 | } 130 | self::$_loaded = true; 131 | return self::$_loaded; 132 | } 133 | 134 | /** 135 | * Attributes for a factory 136 | * 137 | * @param string $name name of the factory 138 | * 139 | * @return mixed attributes 140 | */ 141 | public static function attributes_for($name) 142 | { 143 | self::load(); 144 | $data = self::get($name); 145 | if (!$data) { 146 | return null; 147 | } 148 | return $data; 149 | } 150 | 151 | /** 152 | * Create a factory 153 | * 154 | * @param string $name factory name 155 | * 156 | * @return mixed factory 157 | */ 158 | public static function create($name) 159 | { 160 | return self::build($name, true); 161 | } 162 | 163 | /** 164 | * Build a factory 165 | * 166 | * @param string $name factory name 167 | * @param boolean $create create factory 168 | * 169 | * @return mixed factory 170 | */ 171 | public static function build($name, $create = false) 172 | { 173 | self::load(); 174 | $data = self::attributes_for($name); 175 | if (!$data) { 176 | return null; 177 | } 178 | 179 | // if is a different class ... 180 | if (is_array(self::$_options[$name]) 181 | && array_key_exists("class_name", self::$_options[$name]) 182 | ) { 183 | $name = self::$_options[$name]["class_name"]; 184 | } 185 | 186 | $cls = ucfirst(strtolower($name)); 187 | $obj = new $cls(); 188 | $pk = $obj::getPK(); 189 | 190 | if (!array_key_exists($pk, $data)) { 191 | $data[$pk] = null; 192 | } 193 | 194 | $obj = new $cls($data); 195 | if ($create) { 196 | if (!$obj->isValid()) { 197 | return null; 198 | } 199 | $obj->save(); 200 | } 201 | return $obj; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Cache class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Caching 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Cache 27 | { 28 | private static $_instance = null; 29 | private $_prepared_cache = array(); 30 | private $_timeout = 300; // seconds 31 | private $_last_expired_at = null; 32 | 33 | /** 34 | * Private constructor 35 | * 36 | * @return null 37 | */ 38 | private function __construct() 39 | { 40 | $this->_prepared_cache = array(); 41 | } 42 | 43 | /** 44 | * Get cache instance 45 | * 46 | * @return mixed cache 47 | */ 48 | public static function getInstance() 49 | { 50 | if (!isset(self::$_instance)) { 51 | $c = __CLASS__; 52 | self::$_instance = new $c; 53 | } 54 | return self::$_instance; 55 | } 56 | 57 | /** 58 | * Get the SQL hash 59 | * 60 | * @param string $sql sql query 61 | * 62 | * @return string 63 | */ 64 | private function _sqlHash($sql) 65 | { 66 | return md5($sql); 67 | } 68 | 69 | /** 70 | * Put a prepared statement on cache, if not there. 71 | * 72 | * @param string $sql query 73 | * @param string $cls class to resolve connection 74 | * 75 | * @return object prepared statement 76 | */ 77 | public function put($sql, $cls = null) 78 | { 79 | $this->expireCache(); 80 | $hash = self::_sqlHash($sql); 81 | 82 | if (array_key_exists($hash, $this->_prepared_cache)) { 83 | Log::log("SQL: already prepared:\n$sql"); 84 | return $this->_prepared_cache[$hash]["statement"]; 85 | } else { 86 | Log::log("SQL: inserting on cache: $sql"); 87 | } 88 | $cls = $cls ? $cls : "TORM\Model"; 89 | $con = $cls::resolveConnection(); 90 | $prepared = $con->prepare($sql); 91 | $this->_prepared_cache[$hash] = ["statement" => $prepared, "timestamp" => time()]; 92 | return $prepared; 93 | } 94 | 95 | /** 96 | * Get a prepared statement from cache 97 | * 98 | * @param string $sql query 99 | * 100 | * @return object or null if not on cache 101 | */ 102 | public function get($sql) 103 | { 104 | $this->expireCache(); 105 | $hash = self::_sqlHash($sql); 106 | 107 | // if doesn't exists, return null 108 | if (!array_key_exists($hash, $this->_prepared_cache)) { 109 | return null; 110 | } 111 | return $this->_prepared_cache[$hash]["statement"]; 112 | } 113 | 114 | /** 115 | * Expire cache 116 | * 117 | * I'd love to use threads to do that, but I'm not forcing 118 | * compiling PHP to use that. 119 | * 120 | * @param boolean $verbose mode 121 | * 122 | * @return boolean 123 | */ 124 | public function expireCache($verbose = false) 125 | { 126 | // if first run ... 127 | if (!$this->_last_expired_at) { 128 | $this->_last_expired_at = time(); 129 | if ($verbose) { 130 | echo "First time running cache expiration\n"; 131 | } 132 | } 133 | 134 | // if current time is lower than the last time ran plust timeout 135 | if (time() < $this->_last_expired_at + $this->_timeout) { 136 | if ($verbose) { 137 | echo "Not checking cache timeouts\n"; 138 | } 139 | return false; 140 | } 141 | 142 | if ($verbose) { 143 | echo "Checking cache timeouts.\n"; 144 | } 145 | foreach ($this->_prepared_cache as $key => $cache) { 146 | if (time() > intval($cache["timestamp"]) + $this->_timeout) { 147 | Model::closeCursor($cache["statement"]); 148 | unset($this->_prepared_cache[$key]); 149 | } 150 | } 151 | $this->_last_expired_at = time(); 152 | return true; 153 | } 154 | 155 | /** 156 | * Return the size of the cache 157 | * 158 | * @return integer 159 | */ 160 | public function size() 161 | { 162 | return sizeof($this->_prepared_cache); 163 | } 164 | 165 | /** 166 | * Clear cache 167 | * 168 | * @return null 169 | */ 170 | public function clear() 171 | { 172 | $this->_prepared_cache = array(); 173 | $this->_last_expired_at = time(); 174 | } 175 | 176 | /** 177 | * Set timeout 178 | * 179 | * @param int $timeout timeout in seconds 180 | * 181 | * @return null 182 | */ 183 | public function setTimeout($timeout) 184 | { 185 | $this->_timeout = $timeout; 186 | } 187 | 188 | /** 189 | * Get timeout 190 | * 191 | * @return int timeout 192 | */ 193 | public function getTimeout() 194 | { 195 | return $this->_timeout; 196 | } 197 | } 198 | ?> 199 | -------------------------------------------------------------------------------- /src/Sequences.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Sequences 16 | { 17 | private static $_sequence = array(); 18 | private static $_sequence_exists = array(); 19 | 20 | /** 21 | * Set the sequence name, if any 22 | * 23 | * @param string $name of the sequence 24 | * 25 | * @return null 26 | */ 27 | public static function setSequenceName($name) 28 | { 29 | $cls = get_called_class(); 30 | self::$_sequence[$cls] = $name; 31 | } 32 | 33 | /** 34 | * Returns the sequence name, if any 35 | * 36 | * @return name of the sequence 37 | */ 38 | public static function getSequenceName() 39 | { 40 | $cls = get_called_class(); 41 | if (!array_key_exists($cls, self::$_sequence)) { 42 | return null; 43 | } 44 | return self::$_sequence[$cls]; 45 | } 46 | 47 | /** 48 | * Resolve the sequence name, if any 49 | * 50 | * @return name of the sequence 51 | */ 52 | public static function resolveSequenceName() 53 | { 54 | if (Driver::$primary_key_behaviour != Driver::PRIMARY_KEY_SEQUENCE) { 55 | return null; 56 | } 57 | 58 | $name = self::getSequenceName(); 59 | if ($name) { 60 | return $name; 61 | } 62 | 63 | $table = strtolower(self::getTableName()); 64 | $pk = self::getPK(); 65 | 66 | // need to deal with specific databases here. can't open the Driver 67 | // class and rewrite the method like we do in Ruby 68 | switch (Driver::$name) { 69 | case "oracle": 70 | $suffix = "_sequence"; 71 | return $table.$suffix; 72 | case "postgresql": 73 | return "{$table}_{$pk}_seq"; 74 | } 75 | return null; 76 | } 77 | 78 | /** 79 | * Check if a sequence exists 80 | * 81 | * @return exists or not 82 | */ 83 | private static function _sequenceExists() 84 | { 85 | if (Driver::$primary_key_behaviour != Driver::PRIMARY_KEY_SEQUENCE) { 86 | return null; 87 | } 88 | 89 | // caching if the sequence exists 90 | $cls = get_called_class(); 91 | $name = self::resolveSequenceName(); 92 | 93 | if (array_key_exists($cls, self::$_sequence_exists) 94 | && array_key_exists($name, self::$_sequence_exists[$cls]) 95 | ) { 96 | return true; 97 | } 98 | 99 | $rtn = false; 100 | 101 | switch (Driver::$name) { 102 | case "oracle": 103 | $rtn = self::_oracleSequenceExists($name); 104 | break; 105 | case "postgresql": 106 | $rtn = self::_postgresqlSequenceExists($name); 107 | break; 108 | } 109 | 110 | // if exists, cache result 111 | if ($rtn) { 112 | if (!array_key_exists($cls, self::$_sequence_exists)) { 113 | self::$_sequence_exists[$cls] = array(); 114 | } 115 | self::$_sequence_exists[$cls][$name] = true; 116 | } 117 | return $rtn; 118 | } 119 | 120 | /** 121 | * Check if an Oracle sequence exists 122 | * 123 | * @param string $name sequence name 124 | * 125 | * @return exists or not 126 | */ 127 | private function _oracleSequenceExists($name) 128 | { 129 | $escape = Driver::$escape_char; 130 | $sql = "select count(sequence_name) as $escape"."CNT"."$escape from user_sequences where sequence_name='$name' or sequence_name='".strtolower($name)."' or sequence_name='".strtoupper($name)."'"; 131 | $stmt = self::query($sql); 132 | $rst = $stmt->fetch(\PDO::FETCH_ASSOC); 133 | $rtn = intval($rst["CNT"]) > 0; 134 | self::closeCursor($stmt); 135 | return $rtn; 136 | } 137 | 138 | /** 139 | * Check if an PostgreSQL sequence exists 140 | * 141 | * @param string $name sequence name 142 | * 143 | * @return exists or not 144 | */ 145 | private function _postgresqlSequenceExists($name) 146 | { 147 | $escape = Driver::$escape_char; 148 | $sql = "select count(*) as {$escape}CNT{$escape} from information_schema.sequences where sequence_name = '$name'"; 149 | $stmt = self::query($sql); 150 | $rst = $stmt->fetch(\PDO::FETCH_ASSOC); 151 | $rtn = intval($rst["CNT"]) > 0; 152 | self::closeCursor($stmt); 153 | return $rtn; 154 | } 155 | 156 | /** 157 | * Create a sequence if not exists 158 | * 159 | * @return null 160 | */ 161 | private static function _checkSequence() 162 | { 163 | if (Driver::$primary_key_behaviour!=Driver::PRIMARY_KEY_SEQUENCE) { 164 | return null; 165 | } 166 | 167 | if (self::_sequenceExists()) { 168 | return; 169 | } 170 | 171 | switch (Driver::$name) { 172 | case "oracle": 173 | self::_createOracleSequence(); 174 | break; 175 | case "postgresql": 176 | self::_createPostgresqlSequence(); 177 | break; 178 | } 179 | } 180 | 181 | /** 182 | * Create an Oracle sequence 183 | * 184 | * @return null 185 | */ 186 | private static function _createOracleSequence() 187 | { 188 | $name = self::resolveSequenceName(); 189 | $sql = "create sequence $name increment by 1 start with 1 nocycle nocache"; 190 | Log::log($sql); 191 | $stmt = self::query($sql); 192 | self::closeCursor($stmt); 193 | } 194 | 195 | /** 196 | * Create a PostgreSQL sequence 197 | * 198 | * @return null 199 | */ 200 | private static function _createPostgresqlSequence() 201 | { 202 | $name = self::resolveSequenceName(); 203 | $sql = "create sequence $name increment by 1 start with 1 no cycle"; 204 | Log::log($sql); 205 | $stmt = self::query($sql); 206 | self::closeCursor($stmt); 207 | } 208 | 209 | /** 210 | * Get the next value from a sequence 211 | * 212 | * @param string $name sequence name 213 | * 214 | * @return mixed next value 215 | */ 216 | public static function sequenceNextVal($name) 217 | { 218 | $sql = null; 219 | 220 | switch (Driver::$name) { 221 | case "oracle": 222 | $sql = "select $name.nextval from dual"; 223 | break; 224 | case "postgresql": 225 | $sql = "select nextval('$name') as nextval"; 226 | break; 227 | } 228 | 229 | if ($sql == null) { 230 | return null; 231 | } 232 | 233 | $stmt = self::executePrepared($sql); 234 | $data = $stmt->fetch(\PDO::FETCH_ASSOC); 235 | $rtn = null; 236 | 237 | if (!$data) { 238 | return null; 239 | } 240 | 241 | $seq_keys = array("nextval", "NEXTVAL"); 242 | foreach ($seq_keys as $seq_key) { 243 | if (array_key_exists($seq_key, $data)) { 244 | $rtn = $data[$seq_key]; 245 | break; 246 | } 247 | } 248 | return $rtn; 249 | } 250 | } 251 | ?> 252 | -------------------------------------------------------------------------------- /src/HasMany.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait HasMany 16 | { 17 | private static $_has_many = array(); 18 | private static $_has_many_maps = array(); 19 | private $_has_many_ids = array(); 20 | 21 | /** 22 | * Create a has many relationship 23 | * 24 | * @param string $attr attribute 25 | * @param mixed $options to use 26 | * 27 | * @return null 28 | */ 29 | public static function hasMany($attr, $options=null) 30 | { 31 | $cls = get_called_class(); 32 | 33 | if (!array_key_exists($cls, self::$_has_many)) { 34 | self::$_has_many[$cls] = array(); 35 | } 36 | 37 | self::$_has_many[$cls][$attr] = $options ? $options : false; 38 | 39 | $klass = self::hasManyClass($attr); 40 | $ids = strtolower($klass)."_ids"; 41 | self::$_has_many_maps[$cls][$ids] = $attr; 42 | } 43 | 44 | /** 45 | * Check if there is a has many association 46 | * 47 | * @param string $attr attribute to check 48 | * 49 | * @return boolean 50 | */ 51 | public static function hasHasMany($attr) 52 | { 53 | $cls = get_called_class(); 54 | return array_key_exists($cls, self::$_has_many) && 55 | array_key_exists($attr, self::$_has_many[$cls]); 56 | } 57 | 58 | /** 59 | * Check a has many association and returns it resolved, if exists. 60 | * 61 | * @param string $method name 62 | * @param mixed $value of this object primary key 63 | * 64 | * @return has many collection, if any 65 | */ 66 | private static function _checkAndReturnMany($method, $value) 67 | { 68 | $cls = get_called_class(); 69 | if (array_key_exists($cls, self::$_has_many) 70 | && array_key_exists($method, self::$_has_many[$cls]) 71 | ) { 72 | return self::_resolveHasMany($method, $value); 73 | } 74 | } 75 | 76 | /** 77 | * Check class from a relation, like hasManyClass("tickets") => "Ticket" 78 | * 79 | * @param string $attr attribute to check 80 | * 81 | * @return the class 82 | */ 83 | public static function hasManyClass($attr) 84 | { 85 | if (!self::hasHasMany($attr)) { 86 | return null; 87 | } 88 | 89 | $cls = get_called_class(); 90 | $configs = self::$_has_many[$cls][$attr]; 91 | $klass = is_array($configs) && array_key_exists("class_name", $configs) ? $configs["class_name"] : ucfirst(preg_replace('/s$/', "", $attr)); 92 | return $klass; 93 | } 94 | 95 | /** 96 | * Check if there is a has many foreign key 97 | * 98 | * @param string $attr attribute 99 | * 100 | * @return string foreign key 101 | */ 102 | public static function hasManyForeignKey($attr) 103 | { 104 | if (!self::hasHasMany($attr)) { 105 | return null; 106 | } 107 | 108 | $cls = get_called_class(); 109 | $configs = self::$_has_many[$cls][$attr]; 110 | $key = is_array($configs) && array_key_exists("foreign_key", $configs) ? $configs["foreign_key"] : (self::isIgnoringCase() ? strtolower($cls)."_id" : $cls."_id"); 111 | return $key; 112 | } 113 | 114 | /** 115 | * Resolve the has many association and returns the collection with values 116 | * 117 | * @param string $attr association name 118 | * @param mixed $value of this object 119 | * 120 | * @return collection 121 | */ 122 | private static function _resolveHasMany($attr, $value) 123 | { 124 | $cls = get_called_class(); 125 | if (!self::hasHasMany($attr)) { 126 | return null; 127 | } 128 | 129 | $configs = self::$_has_many[$cls][$attr]; 130 | $has_many_cls = self::hasManyClass($attr); 131 | $this_key = self::hasManyForeignKey($attr); 132 | $collection = $has_many_cls::where(array($this_key => $value)); 133 | return $collection; 134 | } 135 | 136 | /** 137 | * Resolve has many ids 138 | * 139 | * @param string $attr attribute name 140 | * @param mixed $values to resolve 141 | * 142 | * @return ids 143 | */ 144 | private function _resolveIds($attr, $values=null) 145 | { 146 | $cls = get_called_class(); 147 | 148 | if (!array_key_exists($cls, self::$_has_many_maps) 149 | || !array_key_exists($attr, self::$_has_many_maps[$cls]) 150 | ) { 151 | return null; 152 | } 153 | 154 | $klass = self::hasManyClass(self::$_has_many_maps[$cls][$attr]); 155 | $foreign = self::hasManyForeignKey(Inflections::pluralize(strtolower($klass))); 156 | $value = $this->_data[self::getPK()]; 157 | $klasspk = $klass::getPK(); 158 | 159 | // if values sent, set them 160 | if ($values) { 161 | $this->_has_many_ids = $values; 162 | $ids = join(",", $values); 163 | $this->_nullNotPresentIds($klass, $foreign, $ids, $value); 164 | } else { 165 | $data = $klass::where(array($foreign => $value)); 166 | $this->_has_many_ids = array(); 167 | while ($row=$data->next()) { 168 | array_push($this->_has_many_ids, $row->get($klasspk)); 169 | } 170 | } 171 | return $this->_has_many_ids; 172 | } 173 | 174 | /** 175 | * Set values to a has many association, from an array 176 | * 177 | * @param string $attr attribute 178 | * @param mixed $values values to use 179 | * 180 | * @return collections 181 | */ 182 | private function _resolveCollection($attr, $values) 183 | { 184 | $cls = get_called_class(); 185 | 186 | if (!array_key_exists($cls, self::$_has_many_maps)) { 187 | return null; 188 | } 189 | 190 | $maps = array_values(self::$_has_many_maps[$cls]); 191 | if (!in_array($attr, $maps)) { 192 | return null; 193 | } 194 | 195 | if (!$values || !is_array($values) || sizeof($values)<1 || !is_object($values[0])) { 196 | return null; 197 | } 198 | 199 | $this->_has_many_ids = array(); 200 | 201 | foreach ($values as $value) { 202 | $klass = get_class($value); 203 | $this->push($value); 204 | $id = $value->get($klass::getPK()); 205 | if ($id) { 206 | array_push($this->_has_many_ids, $id); 207 | } 208 | } 209 | return $this->_has_many_ids; 210 | } 211 | 212 | /** 213 | * Nullify foreign class keys not present in array 214 | * 215 | * @param string $klass association class 216 | * @param string $foreign foreign key on association class 217 | * @param mixed $ids ids to check 218 | * @param string $id id of the foreign association 219 | * 220 | * @return null 221 | */ 222 | private function _nullNotPresentIds($klass, $foreign, $ids, $id) 223 | { 224 | $escape = Driver::$escape_char; 225 | $klasspk = $klass::getPK(); 226 | $klass = strtolower($klass); 227 | $table = Model::getTableName($klass); 228 | $sql = "update $escape$table$escape set $escape$foreign$escape=null where $escape$foreign$escape=$id and $escape$table$escape.$escape$klasspk$escape not in ($ids)"; 229 | $stmt = self::query($sql); 230 | self::closeCursor($stmt); 231 | } 232 | 233 | /** 234 | * Push an objet to a has many association 235 | * 236 | * @param mixed $obj object 237 | * 238 | * @return pushed 239 | */ 240 | public function push($obj) 241 | { 242 | if (!$obj) { 243 | return; 244 | } 245 | 246 | $cls = get_called_class(); 247 | $escape = Driver::$escape_char; 248 | $value = array_key_exists(self::getPK(), $this->_data) ? $this->_data[self::getPK()] : null; 249 | $other_cls = get_class($obj); 250 | $other_pk = $other_cls::getPK(); 251 | $other_value = $obj->get($other_pk); 252 | $table = Model::getTableName($other_cls); 253 | $foreign = self::hasManyForeignKey(Inflections::pluralize(strtolower($other_cls))); 254 | 255 | // if the current object exists ... 256 | if (!is_null($value)) { 257 | $obj->set(strtolower($foreign), $value); 258 | 259 | // if the pushed object is still not saved 260 | if (is_null($other_value)) { 261 | if (!$obj->save()) { 262 | return false; 263 | } 264 | $other_value = $obj->get($other_pk); 265 | } 266 | 267 | $foreign = self::$_mapping[$other_cls][$foreign]; 268 | $other_pk = self::$_mapping[$other_cls][$other_pk]; 269 | $sql = "update $escape$table$escape set $escape$foreign$escape=$value where $escape$other_pk$escape=$other_value"; 270 | $stmt = self::query($sql); 271 | $rst = $stmt->rowCount()==1; 272 | self::closeCursor($stmt); 273 | return $rst; 274 | } 275 | 276 | // if current object does not exists ... 277 | if (is_null($value)) { 278 | $this->_pushLater($obj); 279 | } 280 | } 281 | 282 | /** 283 | * Send an object to push later 284 | * 285 | * @param mixed $obj object 286 | * 287 | * @return null 288 | */ 289 | private function _pushLater($obj) 290 | { 291 | array_push($this->_push_later, $obj); 292 | } 293 | } 294 | ?> 295 | -------------------------------------------------------------------------------- /src/Persistence.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | trait Persistence 16 | { 17 | /** 18 | * Save or update currenct object 19 | * 20 | * @param boolean $force to force saving 21 | * 22 | * @return boolean saved/updated 23 | */ 24 | public function save($force=false) 25 | { 26 | if (!self::$_loaded) { 27 | self::_loadColumns(); 28 | } 29 | 30 | // check for callbacks before validation below 31 | $calling = get_called_class(); 32 | if (!self::_checkCallback($calling, "before_save", $this)) { 33 | return false; 34 | } 35 | 36 | // with all the before callbacks checked, check if its valid 37 | if (!$this->isValid()) { 38 | return false; 39 | } 40 | 41 | $pk = $calling::isIgnoringCase() ? strtolower($calling::getPK()) : $calling::getPK(); 42 | $pk_value = array_key_exists($pk, $this->_data) ? $this->_data[$pk] : null; 43 | $attrs = $this->_data; 44 | 45 | if (!$pk_value) { 46 | // if there is a method to get the new primary key value on the class, 47 | // call it 48 | if (method_exists($calling, "getNewPKValue")) { 49 | $pk_value = $calling::getNewPKValue(); 50 | if (!$this->_data[$pk]) { 51 | $this->_data[$pk] = $pk_value; 52 | } 53 | $attrs = $this->_data; 54 | } 55 | } 56 | 57 | // if found a primary key value, check if exists 58 | if ($pk_value) { 59 | $this->_new_rec = !self::find($pk_value); 60 | } 61 | 62 | $rst = false; 63 | $newr = false; 64 | 65 | if ($this->_new_rec) { 66 | $newr = true; 67 | self::_checkCallback($calling, "before_create", $this); 68 | $rst = $this->_insert($attrs, $calling, $pk, $pk_value); 69 | } else { 70 | self::_checkCallback($calling, "before_update", $this); 71 | 72 | // no need to update if there weren't changes 73 | if (sizeof($this->changed()) < 1 && !$force) { 74 | Log::log("No changes, not updating"); 75 | $rst = true; 76 | } else { 77 | $rst = $this->_update($attrs, $calling, $pk, $pk_value); 78 | } 79 | } 80 | 81 | if ($rst) { 82 | self::_checkCallback($calling, "after_save", $this); 83 | if ($newr) { 84 | self::_checkCallback($calling, "after_create", $this); 85 | } else { 86 | self::_checkCallback($calling, "after_update", $this); 87 | } 88 | } 89 | $this->_prev_data = $this->_data; 90 | return $rst; 91 | } 92 | 93 | /** 94 | * Insert a new record 95 | * 96 | * @param mixed $attrs attributes 97 | * @param string $calling class 98 | * @param string $pk primary key 99 | * @param mixed $pk_value primary key value 100 | * 101 | * @return inserted or not 102 | */ 103 | private function _insert($attrs, $calling, $pk, $pk_value) 104 | { 105 | $escape = Driver::$escape_char; 106 | $vals = array(); 107 | $create_column = self::hasColumn("created_at"); 108 | $update_column = self::hasColumn("updated_at"); 109 | 110 | $sql = "insert into $escape".$calling::getTableName()."$escape ("; 111 | 112 | // remove the current value when need to insert a NULL value to create 113 | // the autoincrement value 114 | if (Driver::$primary_key_behaviour == Driver::PRIMARY_KEY_DELETE && !$pk_value) { 115 | unset($attrs[$pk]); 116 | } 117 | 118 | if (Driver::$primary_key_behaviour == Driver::PRIMARY_KEY_SEQUENCE && empty($pk_value)) { 119 | $seq_name = self::resolveSequenceName(); 120 | 121 | // check if the sequence exists 122 | self::_checkSequence(); 123 | if (!self::_sequenceExists()) { 124 | $this->_addError($pk, "Sequence $seq_name could not be created"); 125 | return false; 126 | } 127 | 128 | // get the sequence next value 129 | $attrs[$pk] = self::sequenceNextVal($seq_name); 130 | } 131 | 132 | // use sequence, but there is already a value on the primary key. 133 | // remember that it will allow this only if is really a record that 134 | // wasn't found when checking for the primary key, specifying that its 135 | // a new record! 136 | if (Driver::$primary_key_behaviour == Driver::PRIMARY_KEY_SEQUENCE && !empty($pk_value)) { 137 | $attrs[$pk] = $pk_value; 138 | } 139 | 140 | if ($create_column && array_key_exists($create_column, $attrs)) { 141 | unset($attrs[$create_column]); 142 | } 143 | 144 | if ($update_column && array_key_exists($update_column, $attrs)) { 145 | unset($attrs[$update_column]); 146 | } 147 | 148 | // marks to insert values on prepared statement 149 | $marks = array(); 150 | 151 | foreach ($attrs as $attr => $value) { 152 | $sql .= "$escape".self::$_mapping[$calling][$attr]."$escape,"; 153 | array_push($marks, "?"); 154 | } 155 | 156 | // if is there a 'created_at' column ... 157 | if ($create_column) { 158 | $sql .= "$escape".self::$_mapping[$calling][$create_column]."$escape,"; 159 | array_push($marks, Driver::$current_timestamp); 160 | } 161 | 162 | // if is there an 'updated_at' column ... 163 | if ($update_column) { 164 | $sql .= "$escape".self::$_mapping[$calling][$update_column]."$escape,"; 165 | array_push($marks, Driver::$current_timestamp); 166 | } 167 | 168 | $marks = join(",", $marks); 169 | $sql = substr($sql, 0, strlen($sql)-1); 170 | $sql .= ") values ($marks)"; 171 | 172 | // now fill the $vals array with all values to be inserted on the 173 | // prepared statement 174 | foreach ($attrs as $attr => $value) { 175 | array_push($vals, $value); 176 | } 177 | $rtn = self::executePrepared($sql, $vals)->rowCount()==1; 178 | 179 | // if inserted 180 | if ($rtn) { 181 | // check for last inserted value 182 | $lid = null; 183 | if (Driver::$last_id_supported) { 184 | $lid = self::resolveConnection()->lastInsertId(); 185 | if (empty($this->_data[$pk]) && !empty($lid)) { 186 | $this->_data[$pk] = $lid; 187 | } 188 | } 189 | 190 | // or, like Oracle, if the database does not support last inserted id 191 | if (empty($this->_data[$pk]) && empty($lid) && !empty($attrs[$pk])) { 192 | $this->_data[$pk] = $attrs[$pk]; 193 | } 194 | 195 | // check for database filled columns 196 | if ($this->_data[$pk]) { 197 | $found = self::find($this->_data[$pk]); 198 | if ($found) { 199 | if ($create_column) { 200 | $this->_data[$create_column] = $found->get($create_column); 201 | } 202 | } 203 | } 204 | 205 | // push later objects 206 | foreach ($this->_push_later as $obj) { 207 | $this->push($obj); 208 | } 209 | $this->_push_later = array(); 210 | } 211 | return $rtn; 212 | } 213 | 214 | /** 215 | * Update a record 216 | * 217 | * @param mixed $attrs attributes 218 | * @param string $calling class 219 | * @param string $pk primary key 220 | * @param mixed $pk_value primary key value 221 | * 222 | * @return boolean updated or not 223 | */ 224 | private function _update($attrs,$calling,$pk,$pk_value) 225 | { 226 | $escape = Driver::$escape_char; 227 | $vals = array(); 228 | $update_column = self::hasColumn("updated_at"); 229 | $create_column = self::hasColumn("created_at"); 230 | 231 | // no way to update a primary key! 232 | unset($attrs[$pk]); 233 | 234 | $sql = "update $escape".$calling::getTableName()."$escape set "; 235 | 236 | foreach ($attrs as $attr => $value) { 237 | if (($update_column && $attr == $update_column) 238 | || ($create_column && $attr == $create_column) 239 | ) { 240 | continue; 241 | } 242 | 243 | if (strlen(trim($value)) < 1) { 244 | $value = null; 245 | } 246 | 247 | $sql .= "$escape".self::$_mapping[$calling][$attr]."$escape=?,"; 248 | array_push($vals, $value); 249 | } 250 | 251 | if ($update_column) { 252 | $sql .= "$escape".self::$_mapping[$calling][$update_column]."$escape=".Driver::$current_timestamp.","; 253 | } 254 | 255 | $sql = substr($sql, 0, strlen($sql)-1); 256 | $sql .= " where $escape".self::getTableName()."$escape.$escape".self::$_mapping[$calling][$pk]."$escape=?"; 257 | array_push($vals, $pk_value); 258 | 259 | return self::executePrepared($sql, $vals)->rowCount()==1; 260 | } 261 | 262 | /** 263 | * Destroy the current object 264 | * 265 | * @return boolean destroyed or not 266 | */ 267 | public function destroy() 268 | { 269 | if (!self::$_loaded) { 270 | self::_loadColumns(); 271 | } 272 | 273 | $calling = get_called_class(); 274 | 275 | if (!self::_checkCallback($calling, "before_destroy", $this)) { 276 | return false; 277 | } 278 | 279 | $table_name = $calling::getTableName(); 280 | $pk = $calling::isIgnoringCase() ? strtolower($calling::getPK()) : $calling::getPK(); 281 | $pk_value = $this->_data[$pk]; 282 | $escape = Driver::$escape_char; 283 | $sql = "delete from $escape$table_name$escape where $escape$table_name$escape.$escape".self::$_mapping[$calling][$pk]."$escape=?"; 284 | 285 | $rst = self::executePrepared($sql, array($pk_value))->rowCount()==1; 286 | if ($rst) { 287 | self::_checkCallback($calling, "after_destroy", $this); 288 | } 289 | return $rst; 290 | } 291 | 292 | /** 293 | * Update object attributes 294 | * 295 | * @param mixed $attrs attributes 296 | * 297 | * @return updated or not 298 | */ 299 | public function updateAttributes($attrs) 300 | { 301 | if (array_key_exists(self::getPK(), $attrs)) { 302 | unset($attrs[self::getPK()]); 303 | } 304 | foreach ($attrs as $attr => $value) { 305 | $this->_data[$attr] = $value; 306 | } 307 | return $this->save(); 308 | } 309 | } 310 | ?> 311 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Collection main class 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Collections 21 | * @package TORM 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Collection implements \Iterator 27 | { 28 | private $_data = null; 29 | private $_builder = null; 30 | private $_vals = null; 31 | private $_cls = null; 32 | private $_curval = null; 33 | private $_count = null; 34 | 35 | public $page = null; 36 | public $per_page = null; 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @param mixed $builder builder object 42 | * @param mixed $vals values 43 | * @param string $cls class 44 | */ 45 | public function __construct($builder, $vals, $cls) 46 | { 47 | $this->_data = null; 48 | $this->_builder = $builder; 49 | $this->_vals = $vals; 50 | $this->_cls = $cls; 51 | $this->_count = 0; 52 | } 53 | 54 | /** 55 | * Define row limit 56 | * 57 | * @param int $limit row limit 58 | * 59 | * @return mixed this object 60 | */ 61 | public function limit($limit) 62 | { 63 | $this->_builder->limit = $limit; 64 | return $this; 65 | } 66 | 67 | /** 68 | * Define row order 69 | * 70 | * @param int $order row order 71 | * 72 | * @return mixed this object 73 | */ 74 | function order($order) 75 | { 76 | $this->_builder->order = $order; 77 | return $this; 78 | } 79 | 80 | /** 81 | * Return current value 82 | * 83 | * @return mixed value 84 | */ 85 | public function current() 86 | { 87 | if ($this->_curval == null) { 88 | return $this->next(); 89 | } 90 | return new $this->_cls($this->_curval); 91 | } 92 | 93 | /** 94 | * Return current key 95 | * 96 | * @return mixed key 97 | */ 98 | public function key() 99 | { 100 | return $this->_count; 101 | } 102 | 103 | /** 104 | * Return if its valid 105 | * 106 | * @return boolean valid 107 | */ 108 | public function valid() 109 | { 110 | return $this->current() != null && $this->_curval != null; 111 | } 112 | 113 | /** 114 | * Rewind collection 115 | * 116 | * @return null 117 | */ 118 | public function rewind() 119 | { 120 | $this->_count = 0; 121 | } 122 | 123 | /** 124 | * Return collection row count 125 | * 126 | * Example: 127 | * echo Person::all()->count(); 128 | * 129 | * @return int row count 130 | */ 131 | public function count() 132 | { 133 | $cls = $this->_cls; 134 | $pk = $cls::getPK(); 135 | $builder = $this->_makeBuilderForAggregations(" count($pk) "); 136 | return $this->_executeAndReturnFirst($builder, $this->_vals); 137 | } 138 | 139 | /** 140 | * Return collection attribute sum 141 | * 142 | * Example: 143 | * echo Person::all()->sum("age"); 144 | * 145 | * @param string $attr attribute 146 | * 147 | * @return mixed sum 148 | */ 149 | public function sum($attr) 150 | { 151 | $builder = $this->_makeBuilderForAggregations(" sum($attr) "); 152 | return $this->_executeAndReturnFirst($builder, $this->_vals); 153 | } 154 | 155 | /** 156 | * Return collection attribute average 157 | * 158 | * Example: 159 | * echo Person::all()->avg("age"); 160 | * 161 | * @param string $attr attribute 162 | * 163 | * @return mixed average 164 | */ 165 | public function avg($attr) 166 | { 167 | $builder = $this->_makeBuilderForAggregations(" avg($attr) "); 168 | return $this->_executeAndReturnFirst($builder, $this->_vals); 169 | } 170 | 171 | /** 172 | * Return collection attribute minimum value 173 | * 174 | * Example: 175 | * echo Person::all()->min("age"); 176 | * 177 | * @param string $attr attribute 178 | * 179 | * @return mixed minimum 180 | */ 181 | public function min($attr) 182 | { 183 | $builder = $this->_makeBuilderForAggregations(" min($attr) "); 184 | return $this->_executeAndReturnFirst($builder, $this->_vals); 185 | } 186 | 187 | /** 188 | * Return collection attribute maximum value 189 | * 190 | * Example: 191 | * echo Person::all()->max("age"); 192 | * 193 | * @param string $attr attribute 194 | * 195 | * @return mixed maximum 196 | */ 197 | public function max($attr) 198 | { 199 | $builder = $this->_makeBuilderForAggregations(" max($attr) "); 200 | return $this->_executeAndReturnFirst($builder, $this->_vals); 201 | } 202 | 203 | /** 204 | * Return collection pagination page 205 | * 206 | * Example: 207 | * echo Person::all()->paginate(1, 25); 208 | * 209 | * @param int $page current page 210 | * @param int $per_page rows per page 211 | * 212 | * @return mixed this object 213 | */ 214 | public function paginate($page, $per_page = 50) 215 | { 216 | $this->_builder->limit = $per_page; 217 | $this->_builder->offset = ($page - 1) * $per_page; 218 | $this->page = $page; 219 | $this->per_page = $per_page; 220 | 221 | if (Driver::$pagination_subquery) { 222 | $this->_builder->limit = $this->_builder->offset + $per_page; 223 | $this->_builder->offset = $this->_builder->offset + 1; 224 | } 225 | return $this; 226 | } 227 | 228 | /** 229 | * Construct builder for aggregations 230 | * 231 | * @param mixed $fields fields to use 232 | * 233 | * @return mixed average 234 | */ 235 | private function _makeBuilderForAggregations($fields) 236 | { 237 | $table = $this->_builder->table; 238 | $where = $this->_builder->where; 239 | $limit = $this->_builder->limit; 240 | $offset = $this->_builder->offset; 241 | 242 | $builder = new Builder(); 243 | $builder->prefix = "select"; 244 | $builder->fields = $fields; 245 | $builder->table = $table; 246 | $builder->where = $where; 247 | $builder->limit = $limit; 248 | $builder->offset = $offset; 249 | return $builder; 250 | } 251 | 252 | /** 253 | * Return the first value 254 | * 255 | * @param mixed $builder builder 256 | * @param mixed $vals values to use 257 | * 258 | * @return mixed average 259 | */ 260 | private function _executeAndReturnFirst($builder,$vals) 261 | { 262 | $cls = $this->_cls; 263 | $stmt = $cls::executePrepared($builder, $this->_vals); 264 | $data = $stmt->fetch(); 265 | return $data ? $data[0] : 0; 266 | } 267 | 268 | /** 269 | * Destroy collection records 270 | * 271 | * Example: 272 | * Person::all()->destroy(); 273 | * 274 | * @return destroyed or not 275 | */ 276 | public function destroy() 277 | { 278 | $table = $this->_builder->table; 279 | $where = $this->_builder->where; 280 | 281 | $builder = new Builder(); 282 | $builder->prefix = "delete"; 283 | $builder->fields = ""; 284 | $builder->table = $table; 285 | $builder->where = $where; 286 | 287 | $cls = $this->_cls; 288 | return $cls::executePrepared($builder, $this->_vals)->rowCount()>0; 289 | } 290 | 291 | /** 292 | * Update collection attributes 293 | * 294 | * Example: 295 | * echo Person::all()->updateAttributes("age", 25); 296 | * 297 | * @param string $attrs attributes 298 | * 299 | * @return updated or not 300 | */ 301 | public function updateAttributes($attrs) 302 | { 303 | $cls = $this->_cls; 304 | $table = $this->_builder->table; 305 | $where = $this->_builder->where; 306 | $escape = Driver::$escape_char; 307 | 308 | $sql = "update $escape$table$escape set "; 309 | $sql .= $cls::extractUpdateColumns($attrs, ","); 310 | $vals = $cls::extractWhereValues($attrs); 311 | 312 | if (!empty($where)) { 313 | $sql .= " where $where"; 314 | } 315 | $nval = array_merge($vals, is_array($this->_vals) ? $this->_vals : array()); 316 | return $cls::executePrepared($sql, $nval); 317 | } 318 | 319 | /** 320 | * Convert collection to an array 321 | * 322 | * Example: 323 | * echo Person::all()->toArray(); 324 | * 325 | * @param string $limit -1 to all collection, otherwise the number of 326 | * elements 327 | * 328 | * @return mixed average 329 | */ 330 | public function toArray($limit=-1) 331 | { 332 | $ar = array(); 333 | $cnt = 0; 334 | 335 | while ($data=$this->next()) { 336 | array_push($ar, $data); 337 | $cnt ++; 338 | if ($limit != -1 && $cnt >= $limit) { 339 | break; 340 | } 341 | } 342 | return $ar; 343 | } 344 | 345 | /** 346 | * Get the next result from collection 347 | * 348 | * @return mixed result 349 | */ 350 | private function _getCurrentData() 351 | { 352 | $cls = $this->_cls; 353 | if (!$this->_data) { 354 | $this->_data = $cls::executePrepared($this->_builder, $this->_vals); 355 | } 356 | return $this->_data->fetch(\PDO::FETCH_ASSOC); 357 | } 358 | 359 | /** 360 | * Return the next collection object 361 | * 362 | * Example: 363 | * echo Person::all()->next(); 364 | * 365 | * @return mixed object 366 | */ 367 | public function next() 368 | { 369 | $cls = $this->_cls; 370 | $data = $this->_getCurrentData(); 371 | 372 | if (!$data) { 373 | $this->_curval = null; 374 | return $this->_curval; 375 | } else { 376 | ++$this->_count; 377 | $this->_curval = $data; 378 | return new $this->_cls($this->_curval); 379 | } 380 | } 381 | 382 | /** 383 | * Call a method 384 | * 385 | * @param string $method to call 386 | * @param mixed $args arguments to send 387 | * 388 | * @return method return 389 | */ 390 | public function __call($method, $args) 391 | { 392 | if (!$this->_cls) { 393 | return null; 394 | } 395 | $cls = $this->_cls; 396 | $conditions = Model::getScope($method, $cls); 397 | if (!$conditions) { 398 | return $this; 399 | } 400 | if (is_callable($conditions)) { 401 | $conditions = $conditions($args); 402 | } 403 | $this->_builder->where .= " and $conditions"; 404 | return $this; 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.gnu.org/copyleft/gpl.html GPL 11 | * @link http://github.com/taq/torm 12 | */ 13 | namespace TORM; 14 | 15 | /** 16 | * Model class, the heart of the system 17 | * 18 | * PHP version 5.5 19 | * 20 | * @category Model 21 | * @package Torm 22 | * @author Eustáquio Rangel 23 | * @license http://www.gnu.org/copyleft/gpl.html GPL 24 | * @link http://github.com/taq/torm 25 | */ 26 | class Model 27 | { 28 | use Finders, Storage, Persistence, Errors, Validations, Scopes, HasMany, 29 | HasOne, BelongsTo, Sequences, Callbacks, Dirty; 30 | 31 | const CURSOR_NOTHING = 0; 32 | const CURSOR_CLOSE = 1; 33 | const CURSOR_NULL = 2; 34 | 35 | public static $connection = null; 36 | public static $yaml_file = null; 37 | public $errors = array(); 38 | 39 | private static $_table_name = array(); 40 | private static $_order = array(); 41 | private static $_pk = array(); 42 | private static $_columns = array(); 43 | private static $_ignorecase = array(); 44 | private static $_mapping = array(); 45 | private static $_loaded = array(); 46 | private static $_cc_action = self::CURSOR_CLOSE; 47 | private static $_connections = array(); 48 | 49 | private $_new_rec = false; 50 | private $_push_later = array(); 51 | 52 | /** 53 | * Constructor 54 | * 55 | * If data is sent, then it loads columns with it. 56 | * 57 | * @param array $data to fill the object 58 | * 59 | * @package TORM 60 | */ 61 | public function __construct($data=null) 62 | { 63 | $cls = get_called_class(); 64 | self::_checkLoaded(); 65 | 66 | // setting default null values 67 | $this->_data = self::_loadNullValues(); 68 | $this->_prev_data = self::_loadNullValues(); 69 | $this->_orig_data = self::_loadNullValues(); 70 | 71 | // if data not send, is a new record, return 72 | if ($data == null) { 73 | $this->_new_rec = true; 74 | $this->_validateAfterInitialize(); 75 | return; 76 | } 77 | 78 | foreach ($data as $key => $value) { 79 | // not numeric keys 80 | if (preg_match("/^\d+$/", $key)) { 81 | continue; 82 | } 83 | $keyr = $key; 84 | 85 | // if ignoring case, convert all keys to lowercase 86 | if (self::isIgnoringCase()) { 87 | $keyr = strtolower($key); 88 | $data[$keyr] = $value; 89 | if ($keyr != $key) { 90 | unset($data[$key]); 91 | } 92 | } 93 | 94 | // if there is no mapping array, create one 95 | if (!array_key_exists($cls, self::$_mapping)) { 96 | self::$_mapping[$cls] = array(); 97 | } 98 | 99 | // if the key is not on the mapping array, add it 100 | if (!array_key_exists($keyr, self::$_mapping[$cls])) { 101 | self::$_mapping[$cls][$key] = $keyr; 102 | } 103 | } 104 | 105 | $this->_data = $data; 106 | $this->_prev_data = $data; 107 | $this->_orig_data = $data; 108 | 109 | // check if is a new record 110 | $pk = $cls::getPK(); 111 | 112 | // if there is no value on PK, is a new record 113 | if (!array_key_exists($pk, $this->_data) || empty($this->_data[$pk])) { 114 | $this->_new_rec = true; 115 | } 116 | 117 | $this->_validateAfterInitialize(); 118 | } 119 | 120 | /** 121 | * Validate after initialize 122 | * 123 | * @return null 124 | */ 125 | private function _validateAfterInitialize() 126 | { 127 | if (method_exists($this, "afterInitialize")) { 128 | $this->afterInitialize(); 129 | } 130 | } 131 | 132 | /** 133 | * Reload the current object 134 | * 135 | * @return mixed object 136 | */ 137 | public function reload() 138 | { 139 | $data = $this->find($this->id)->getData(); 140 | $this->_setData($data); 141 | $this->_belongs_cache = array(); 142 | } 143 | 144 | /** 145 | * Check if is ignoring case 146 | * 147 | * @return boolean ignoring case 148 | */ 149 | public static function isIgnoringCase() 150 | { 151 | $cls = get_called_class(); 152 | if (!array_key_exists($cls, self::$_ignorecase)) { 153 | return true; 154 | } 155 | return self::$_ignorecase[$cls]; 156 | } 157 | 158 | /** 159 | * Load null values on row columns. Useful to new objects. 160 | * 161 | * @return null 162 | */ 163 | private static function _loadNullValues() 164 | { 165 | $values = array(); 166 | $cls = get_called_class(); 167 | 168 | if (!array_key_exists($cls, self::$_columns)) { 169 | return null; 170 | } 171 | 172 | foreach (self::$_columns[$cls] as $column) { 173 | $name = self::isIgnoringCase() ? strtolower($column) : $column; 174 | $values[$column] = null; 175 | } 176 | return $values; 177 | } 178 | 179 | /** 180 | * Set the model's table name 181 | * 182 | * @param string $table_name table name 183 | * 184 | * @return null 185 | */ 186 | public static function setTableName($table_name) 187 | { 188 | $cls = get_called_class(); 189 | self::$_table_name[$cls] = $table_name; 190 | } 191 | 192 | /** 193 | * Extract namespace from class name 194 | * 195 | * @param string $class class name 196 | * 197 | * @return string $class class name 198 | */ 199 | private static function _extractClassName($cls) 200 | { 201 | $tokens = preg_split("/\\\\/", $cls); 202 | $size = sizeof($tokens); 203 | if ($size < 2) { 204 | return $cls; 205 | } 206 | return $tokens[$size-1]; 207 | } 208 | 209 | /** 210 | * Returns the table name. 211 | * If not specified one, get the current class name and pluralize it. 212 | * 213 | * @param string $cls class name 214 | * 215 | * @return string table name 216 | */ 217 | public static function getTableName($cls=null) 218 | { 219 | $fullcls = $cls ? $cls : get_called_class(); 220 | 221 | if (array_key_exists($fullcls, self::$_table_name)) { 222 | return self::$_table_name[$fullcls]; 223 | } 224 | 225 | $cls = self::_extractClassName($fullcls); 226 | 227 | $name = Inflections::pluralize($cls); 228 | if (self::isIgnoringCase()) { 229 | $name = strtolower($name); 230 | } 231 | return $name; 232 | } 233 | 234 | /** 235 | * Set the primary key column 236 | * 237 | * @param string $pk primary key 238 | * 239 | * @return null 240 | */ 241 | public static function setPK($pk) 242 | { 243 | $cls = get_called_class(); 244 | self::$_pk[$cls] = $pk; 245 | } 246 | 247 | /** 248 | * Returns the primary key column. 249 | * 250 | * @return string primary key 251 | */ 252 | public static function getPK() 253 | { 254 | $cls = get_called_class(); 255 | return array_key_exists($cls, self::$_pk) ? self::$_pk[$cls] : "id"; 256 | } 257 | 258 | /** 259 | * Set the default order 260 | * 261 | * @param string $order default 262 | * 263 | * @return null 264 | */ 265 | public static function setOrder($order) 266 | { 267 | $cls = get_called_class(); 268 | self::$_order[$cls] = $order; 269 | } 270 | 271 | /** 272 | * Returns the default order. 273 | * If not specified, returns an empty string. 274 | * 275 | * @return string order 276 | */ 277 | public static function getOrder() 278 | { 279 | $cls = get_called_class(); 280 | return array_key_exists($cls, self::$_order) ? self::$_order[$cls] : ""; 281 | } 282 | 283 | /** 284 | * Returns the inverse order. 285 | * If DESC is specified, returns ASC. 286 | * 287 | * @return string order 288 | */ 289 | public static function getReversedOrder() 290 | { 291 | $sort = preg_match("/desc/i", self::getOrder()); 292 | $sort = $sort ? " ASC " : " DESC "; 293 | return self::getOrder() ? self::getOrder()." $sort" : ""; 294 | } 295 | 296 | /** 297 | * Sets a specific connection for the current model 298 | * 299 | * @param mixed $con PDO connection 300 | * @param string $env enviroment 301 | * 302 | * @return null 303 | */ 304 | public static function setConnection($con, $env="development") 305 | { 306 | $cls = get_called_class(); 307 | if (!array_key_exists($cls, self::$_connections)) { 308 | self::$_connections[$cls] = array(); 309 | } 310 | self::$_connections[$cls][$env] = $con; 311 | } 312 | 313 | /** 314 | * Resolve the current connection handle. 315 | * Get it from PDO or from the current class. 316 | * 317 | * @return object connection 318 | */ 319 | public static function resolveConnection() 320 | { 321 | $cls = get_called_class(); 322 | $env = Connection::selectEnvironment(); 323 | 324 | if (array_key_exists($cls, self::$_connections) 325 | && array_key_exists($env, self::$_connections[$cls]) 326 | ) { 327 | return self::$_connections[$cls][$env]; 328 | } 329 | return self::$connection ? self::$connection : Connection::getConnection(); 330 | } 331 | 332 | /** 333 | * Load column info 334 | * 335 | * @return null 336 | */ 337 | private static function _loadColumns() 338 | { 339 | if (!self::resolveConnection()) { 340 | return; 341 | } 342 | 343 | $cls = get_called_class(); 344 | self::$_columns[$cls] = array(); 345 | 346 | $escape = Driver::$escape_char; 347 | 348 | // try to create the TORM info table 349 | $type = Driver::$numeric_column; 350 | 351 | // check if the torm table exists 352 | $rst = null; 353 | $check = false; 354 | try { 355 | $rst = self::query("select id from torm_info"); 356 | $check = true; 357 | } catch (\Exception $e) { 358 | } 359 | 360 | // needs to create table 361 | if (!$check || !is_object($rst) || !$rst->fetch()) { 362 | $stmt = self::query("create table torm_info (id $type(1))"); 363 | self::closeCursor($stmt); 364 | } 365 | if (is_object($rst)) { 366 | self::closeCursor($rst); 367 | } 368 | 369 | // insert first value 370 | $rst = self::query("select id from torm_info"); 371 | if (!$rst->fetch()) { 372 | $stmt = self::query("insert into torm_info values (1)"); 373 | self::closeCursor($stmt); 374 | } 375 | self::closeCursor($rst); 376 | 377 | // hack to dont need a query string to get columns 378 | $sql = "select $escape".self::getTableName()."$escape.* from torm_info left outer join $escape".self::getTableName()."$escape on 1=1"; 379 | 380 | if (Driver::$limit_behaviour==Driver::LIMIT_AROUND && Driver::$limit_query) { 381 | $sql = str_replace("%query%", $sql, Driver::$limit_query); 382 | $sql = str_replace("%limit%", 1, $sql); 383 | } 384 | 385 | $rst = self::query($sql); 386 | $keys = array_keys($rst->fetch(\PDO::FETCH_ASSOC)); 387 | 388 | foreach ($keys as $key) { 389 | $keyc = self::isIgnoringCase() ? strtolower($key) : $key; 390 | array_push(self::$_columns[$cls], $keyc); 391 | self::$_mapping[$cls][$keyc] = $key; 392 | } 393 | self::closeCursor($rst); 394 | self::$_loaded[$cls] = true; 395 | } 396 | 397 | /** 398 | * Extract table columns string 399 | * 400 | * @return string columns 401 | */ 402 | public static function extractColumns() 403 | { 404 | self::_checkLoaded(); 405 | $cls = get_called_class(); 406 | $temp_columns = ""; 407 | $escape = Driver::$escape_char; 408 | foreach (self::$_columns[$cls] as $column) { 409 | $temp_columns .= "$escape".self::getTableName()."$escape.$escape".self::$_mapping[$cls][$column]."$escape,"; 410 | } 411 | return substr($temp_columns, 0, strlen($temp_columns)-1); 412 | } 413 | 414 | /** 415 | * Extract update columns string 416 | * 417 | * @param mixed $values to set columns 418 | * 419 | * @return string columns 420 | */ 421 | public static function extractUpdateColumns($values) 422 | { 423 | $cls = get_called_class(); 424 | $temp_columns = ""; 425 | $escape = Driver::$escape_char; 426 | foreach ($values as $key => $value) { 427 | $temp_columns .= "$escape".self::$_mapping[$cls][$key]."$escape=?,"; 428 | } 429 | return substr($temp_columns, 0, strlen($temp_columns)-1); 430 | } 431 | 432 | /** 433 | * Extract where conditions string 434 | * Just used for testing, to avoid change the function name withou the 435 | * underline on all code. This is something PHP should change. 436 | * 437 | * @param mixed $conditions to extract 438 | * 439 | * @return string where conditions 440 | */ 441 | public static function extractWhereConditions($conditions) 442 | { 443 | return self::_extractWhereConditions($conditions); 444 | } 445 | 446 | /** 447 | * Extract where conditions string 448 | * 449 | * @param mixed $conditions to extract 450 | * 451 | * @return string where conditions 452 | */ 453 | private static function _extractWhereConditions($conditions) 454 | { 455 | if (!$conditions) { 456 | return ""; 457 | } 458 | 459 | $cls = get_called_class(); 460 | $escape = Driver::$escape_char; 461 | 462 | if (is_array($conditions)) { 463 | // check if is a regular array or an associative one 464 | if (array_values($conditions) !== $conditions) { 465 | $conditions = self::_extractWhereAssociativeConditions($conditions, $cls, $escape); 466 | } else { 467 | $conditions = self::_extractWhereRegularConditions($conditions, $cls, $escape); 468 | } 469 | } 470 | return $conditions; 471 | } 472 | 473 | /** 474 | * Extract where conditions string, from an associative array 475 | * 476 | * @param mixed $conditions to extract 477 | * @param string $cls class 478 | * @param string $escape escape char 479 | * 480 | * @return string where conditions 481 | */ 482 | private static function _extractWhereAssociativeConditions($conditions, $cls, $escape) 483 | { 484 | $temp_cond = ""; 485 | foreach ($conditions as $key => $value) { 486 | $temp_cond .= "$escape".self::getTableName()."$escape.$escape".self::$_mapping[$cls][$key]."$escape=? and "; 487 | } 488 | return substr($temp_cond, 0, strlen($temp_cond)-5); 489 | } 490 | 491 | /** 492 | * Extract where conditions string, from a regular array 493 | * 494 | * @param mixed $conditions to extract 495 | * @param string $cls class 496 | * @param string $escape escape char 497 | * 498 | * @return string where conditions 499 | */ 500 | private static function _extractWhereRegularConditions($conditions, $cls, $escape) 501 | { 502 | return $conditions[0]; // the string is always the first 503 | } 504 | 505 | 506 | /** 507 | * Extract where values from conditions 508 | * 509 | * @param mixed $conditions to extract values 510 | * 511 | * @return mixed $values 512 | */ 513 | public static function extractWhereValues($conditions) 514 | { 515 | $values = array(); 516 | if (!$conditions) { 517 | return $values; 518 | } 519 | 520 | if (is_array($conditions)) { 521 | if (array_values($conditions) !== $conditions) { 522 | $values = self::_extractWhereAssociativeValues($conditions); 523 | } else { 524 | $values = self::_extractWhereRegularValues($conditions); 525 | } 526 | } 527 | return $values; 528 | } 529 | 530 | /** 531 | * Extract values from an associative array 532 | * 533 | * @param mixed $conditions conditions 534 | * 535 | * @return mixed values 536 | */ 537 | private static function _extractWhereAssociativeValues($conditions) 538 | { 539 | $values = array(); 540 | foreach ($conditions as $key => $value) { 541 | array_push($values, $value); 542 | } 543 | return $values; 544 | } 545 | 546 | /** 547 | * Extract values from a regular array 548 | * 549 | * @param mixed $conditions conditions 550 | * 551 | * @return mixed values 552 | */ 553 | private static function _extractWhereRegularValues($conditions) 554 | { 555 | return array_slice($conditions, 1); 556 | } 557 | 558 | /** 559 | * Create a query builder 560 | * 561 | * @return mixed $builder 562 | */ 563 | private static function _makeBuilder() 564 | { 565 | $builder = new Builder(); 566 | $builder->table = self::getTableName(); 567 | $builder->order = self::getOrder(); 568 | $builder->cls = get_called_class(); 569 | return $builder; 570 | } 571 | 572 | /** 573 | * Tell if its a new object (not saved) 574 | * 575 | * @return boolean new or not 576 | */ 577 | public function is_new() 578 | { 579 | return $this->_new_rec; 580 | } 581 | 582 | /** 583 | * Check if the object columns were loaded 584 | * 585 | * @return boolean loaded or not 586 | */ 587 | private static function _checkLoaded() 588 | { 589 | $cls = get_called_class(); 590 | if (!array_key_exists($cls, self::$_loaded)) { 591 | self::$_loaded[$cls] = false; 592 | } 593 | if (!self::$_loaded[$cls]) { 594 | self::_loadColumns(); 595 | } 596 | } 597 | 598 | /** 599 | * Check if a column exists 600 | * 601 | * @param string $column to check 602 | * 603 | * @return column key 604 | */ 605 | public static function hasColumn($column) 606 | { 607 | $cls = get_called_class(); 608 | $key = null; 609 | $keys = self::$_columns[$cls]; 610 | 611 | foreach ($keys as $ckey) { 612 | $col1 = self::isIgnoringCase() ? strtolower($ckey) : $ckey; 613 | $col2 = self::isIgnoringCase() ? strtolower($column) : $column; 614 | if ($col1==$col2) { 615 | $key = $ckey; 616 | break; 617 | } 618 | } 619 | return $key; 620 | } 621 | 622 | /** 623 | * Execute a prepared statement, trying to get it from cache. 624 | * 625 | * @param mixed $obj object 626 | * @param mixed $values to use 627 | * 628 | * @return object statement 629 | */ 630 | public static function executePrepared($obj, $values=array()) 631 | { 632 | if (!self::$_loaded) { 633 | self::_loadColumns(); 634 | } 635 | 636 | // convert a builder to string 637 | if (!is_string($obj) && get_class($obj) == "TORM\Builder") { 638 | $sql = $obj->toString(); 639 | } 640 | 641 | if (is_string($obj)) { 642 | $sql = $obj; 643 | } 644 | 645 | // if there aren't value marks, execute like a regular query 646 | if (!preg_match('/\?/', $sql)) { 647 | return self::query($sql); 648 | } 649 | 650 | try { 651 | Log::log("SQL: executing prepared: \n$sql\nvalues:\n".join(",", $values)); 652 | } catch (Exception $e) { 653 | Log::log("SQL: executing prepared: \n$sql\nvalues impossible to convert to string"); 654 | } 655 | 656 | $stmt = Cache::getInstance()->put($sql, get_called_class()); 657 | $stmt->execute($values); 658 | return $stmt; 659 | } 660 | 661 | /** 662 | * Run a SQL query 663 | * 664 | * @param string $sql query 665 | * 666 | * @return mixed result 667 | */ 668 | public static function query($sql) 669 | { 670 | Log::log("SQL:\n$sql"); 671 | return self::resolveConnection()->query($sql); 672 | } 673 | 674 | /** 675 | * Get an attribute value 676 | * 677 | * @param string $attr attribute 678 | * @param boolean $current current value 679 | * 680 | * @return mixed attribute value 681 | */ 682 | public function get($attr,$current=true) 683 | { 684 | if (!$this->_data || !array_key_exists($attr, $this->_data)) { 685 | return null; 686 | } 687 | return Connection::convertToEncoding($current ? $this->_data[$attr] : $this->_prev_data[$attr]); 688 | } 689 | 690 | /** 691 | * Set an attribute value 692 | * 693 | * @param string $attr attribute 694 | * @param mixed $value value 695 | * 696 | * @return null 697 | */ 698 | public function set($attr, $value) 699 | { 700 | $pk = self::getPK(); 701 | // can't change the primary key of an existing record 702 | if (!$this->_new_rec && $attr == $pk) { 703 | return; 704 | } 705 | $this->_data[$attr] = $value; 706 | } 707 | 708 | /** 709 | * Resolve relations on an attribute, if present 710 | * 711 | * @param string $attr attribute 712 | * 713 | * @return null 714 | */ 715 | private function _resolveRelations($attr) 716 | { 717 | $many = self::_checkAndReturnMany($attr, $this->_data[self::getPK()]); 718 | if ($many) { 719 | return $many; 720 | } 721 | 722 | $belongs = $this->_checkAndReturnBelongs($attr, $this->_data); 723 | if ($belongs) { 724 | return $belongs; 725 | } 726 | 727 | $has_one = $this->_checkAndReturnHasOne($attr, $this->_data[self::getPK()]); 728 | if ($has_one) { 729 | return $has_one; 730 | } 731 | return null; 732 | } 733 | 734 | /** 735 | * Trigger to use object values as methods 736 | * Like 737 | * $user->name("john doe"); 738 | * echo $user->name(); 739 | * 740 | * @param string $method method 741 | * @param mixed $args arguments 742 | * 743 | * @return null 744 | */ 745 | public function __call($method, $args) 746 | { 747 | if (method_exists($this, $method)) { 748 | return call_user_func_array(array($this, $method), $args); 749 | } 750 | 751 | $relation = $this->_resolveRelations($method); 752 | if ($relation) { 753 | return $relation; 754 | } 755 | 756 | $ids = $this->_resolveIds($method, $args); 757 | if ($ids) { 758 | return $ids; 759 | } 760 | 761 | if (!$args) { 762 | return $this->get($method); 763 | } 764 | $this->set($method, $args[0]); 765 | } 766 | 767 | /** 768 | * Trigger to get object values as attributes 769 | * Like 770 | * echo $user->name; 771 | * 772 | * @param string $attr attribute 773 | * 774 | * @return attribute value 775 | */ 776 | public function __get($attr) 777 | { 778 | $changes = $this->_changedAttribute($attr); 779 | if ($changes) { 780 | return $changes; 781 | } 782 | 783 | $relation = $this->_resolveRelations($attr); 784 | if ($relation) { 785 | return $relation; 786 | } 787 | 788 | $ids = $this->_resolveIds($attr); 789 | if ($ids) { 790 | return $ids; 791 | } 792 | return $this->get($attr); 793 | } 794 | 795 | /** 796 | * Trigger to set object values as attributes 797 | * Like 798 | * $user->name = "john doe"; 799 | * 800 | * @param string $attr attribute 801 | * @param mixed $value value 802 | * 803 | * @return null 804 | */ 805 | public function __set($attr, $value) 806 | { 807 | $ids = $this->_resolveIds($attr, $value); 808 | if ($ids) { 809 | return $ids; 810 | } 811 | 812 | $ids = $this->_resolveCollection($attr, $value); 813 | if ($ids) { 814 | return $ids; 815 | } 816 | 817 | // if is an object, try to find the belongs association 818 | // if is null, also try to find to nullify it 819 | if (is_object($value) || is_null($value)) { 820 | $bkey = $this->_getBelongsKey($attr); 821 | if (!is_null($bkey) ) { 822 | $attr = $bkey; 823 | if (!is_null($value)) { 824 | $ocls = get_class($value); 825 | $okey = $ocls::getPK(); 826 | $value = $value->get($okey); 827 | } 828 | } 829 | } 830 | $this->set($attr, $value); 831 | } 832 | 833 | /** 834 | * Behaviour to close cursor 835 | * 836 | * @param mixed $action to use 837 | * 838 | * @return null 839 | */ 840 | public static function closeCursorBehaviour($action) 841 | { 842 | self::$_cc_action = $action; 843 | } 844 | 845 | /** 846 | * Close cursor 847 | * 848 | * @param mixed $stmt statement 849 | * 850 | * @return null 851 | */ 852 | public static function closeCursor($stmt) 853 | { 854 | if (self::$_cc_action == self::CURSOR_NOTHING) { 855 | return; 856 | } 857 | 858 | if (self::$_cc_action == self::CURSOR_CLOSE && is_object($stmt)) { 859 | $stmt->closeCursor(); 860 | } else { 861 | $stmt = null; 862 | } 863 | } 864 | 865 | /** 866 | * Return columns 867 | * 868 | * @return mixed columns 869 | */ 870 | public static function getColumns() 871 | { 872 | $cls = get_called_class(); 873 | if (!array_key_exists($cls, self::$_columns)) { 874 | return null; 875 | } 876 | return self::$_columns[$cls]; 877 | } 878 | } 879 | ?> 880 | --------------------------------------------------------------------------------