From 5323886814914d5084a13a7adef1dc6996b464de Mon Sep 17 00:00:00 2001 From: Sven Rymenants Date: Tue, 5 Jan 2016 21:32:04 +0100 Subject: [PATCH 1/3] Support for private columns and filtered objects --- classes/Kohana/ORM.php | 95 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/classes/Kohana/ORM.php b/classes/Kohana/ORM.php index bda79f2..5db9464 100644 --- a/classes/Kohana/ORM.php +++ b/classes/Kohana/ORM.php @@ -244,6 +244,12 @@ public static function factory($model, $id = NULL) */ protected $_errors_filename = NULL; + /** + * List of private columns that will not appear in array or object + * @var array + */ + protected $_private_columns = FALSE; + /** * Constructs a new model and loads a record if given * @@ -808,20 +814,65 @@ public function values(array $values, array $expected = NULL) return $this; } + /** + * Returns the type of the column + * + * @return string + */ + protected function table_column_type($column) + { + if ( ! array_key_exists($column, $this->_table_columns)) + return FALSE; + + return $this->_table_columns[$column]['type']; + } + + /** + * Returns a value as the native type, will return FALSE if the + * value could not be casted. + * + * @return float, int, string or FALSE + */ + protected function get_typed($column) + { + $value = $this->get($column); + + switch($this->table_column_type($column)) + { + case 'float': return floatval($this->__get($column)); + case 'int': return intval($this->__get($column)); + case 'string': return strval($this->__get($column)); + } + + return FALSE; + } + /** * Returns the values of this object as an array, including any related one-one * models that have already been loaded using with() * * @return array */ - public function as_array() + public function as_array($show_all=FALSE) { $object = array(); - foreach ($this->_object as $column => $value) + if ($show_all OR !is_array($this->_private_columns)) { - // Call __get for any user processing - $object[$column] = $this->__get($column); + foreach ($this->_object as $column => $value) + { + // Call __get for any user processing + $object[$column] = $this->__get($column); + } + } + else + { + foreach ($this->_object as $column => $value) + { + // Call __get for any user processing + if (!in_array($column, $this->_private_columns)) + $object[$column] = $this->__get($column); + } } foreach ($this->_related as $column => $model) @@ -833,6 +884,42 @@ public function as_array() return $object; } + /** + * Returns the values of this object as an new object, including any related + * one-one models that have already been loaded using with(). Removes private + * columns. + * + * @return array + */ + public function as_object($show_all=FALSE) + { + $object = new stdClass; + + if ($show_all OR !is_array($this->_private_columns)) + { + foreach ($this->_object as $column => $value) + { + $object->{$column} = $this->get_typed($column); + } + } + else + { + foreach ($this->_object as $column => $value) + { + if (!in_array($column, $this->_private_columns)) + $object->{$column} = $this->get_typed($column); + } + } + + foreach ($this->_related as $column => $model) + { + // Include any related objects that are already loaded + $object->{$column} = $model->as_object(); + } + + return $object; + } + /** * Binds another one-to-one object to this model. One-to-one objects * can be nested using 'object1:object2' syntax From 932b1f4e1bb62e186c468ee9b79202f4811499c4 Mon Sep 17 00:00:00 2001 From: Sven Rymenants Date: Tue, 5 Apr 2016 21:18:06 +0200 Subject: [PATCH 2/3] Support for behaviors --- classes/Kohana/ORM.php | 31 ++ classes/Kohana/ORM/Behavior.php | 44 +++ classes/Kohana/ORM/Behavior/LocalBehavior.php | 58 ++++ classes/ORM/Behavior.php | 3 + classes/ORM/Behavior/Guid.php | 113 +++++++ classes/ORM/Behavior/LocalBehavior.php | 3 + classes/ORM/Behavior/Slug.php | 311 ++++++++++++++++++ 7 files changed, 563 insertions(+) create mode 100644 classes/Kohana/ORM/Behavior.php create mode 100644 classes/Kohana/ORM/Behavior/LocalBehavior.php create mode 100644 classes/ORM/Behavior.php create mode 100644 classes/ORM/Behavior/Guid.php create mode 100644 classes/ORM/Behavior/LocalBehavior.php create mode 100644 classes/ORM/Behavior/Slug.php diff --git a/classes/Kohana/ORM.php b/classes/Kohana/ORM.php index 5db9464..1cce472 100644 --- a/classes/Kohana/ORM.php +++ b/classes/Kohana/ORM.php @@ -250,6 +250,12 @@ public static function factory($model, $id = NULL) */ protected $_private_columns = FALSE; + /** + * List of behaviors + * @var array + */ + protected $_behaviors = array(); + /** * Constructs a new model and loads a record if given * @@ -259,6 +265,13 @@ public function __construct($id = NULL) { $this->_initialize(); + // Invoke all behaviors + foreach ($this->_behaviors as $behavior) + { + if ( ! $behavior->on_construct($this, $id) || $this->_loaded) + return; + } + if ($id !== NULL) { if (is_array($id)) @@ -396,6 +409,12 @@ protected function _initialize() // Clear initial model state $this->clear(); + + // Create the behaviors classes + foreach ($this->behaviors() as $behavior => $behavior_config) + { + $this->_behaviors[] = ORM_Behavior::factory($behavior, $behavior_config); + } } /** @@ -1383,6 +1402,12 @@ public function create(Validation $validation = NULL) if ($this->_loaded) throw new Kohana_Exception('Cannot create :model model because it is already loaded.', array(':model' => $this->_object_name)); + // Invoke all behaviors + foreach ($this->_behaviors as $behavior) + { + $behavior->on_create($this); + } + // Require model validation before saving if ( ! $this->_valid OR $validation) { @@ -1443,6 +1468,12 @@ public function update(Validation $validation = NULL) if ( ! $this->_loaded) throw new Kohana_Exception('Cannot update :model model because it is not loaded.', array(':model' => $this->_object_name)); + // Invoke all behaviors + foreach ($this->_behaviors as $behavior) + { + $behavior->on_update($this); + } + // Run validation if the model isn't valid or we have additional validation rules. if ( ! $this->_valid OR $validation) { diff --git a/classes/Kohana/ORM/Behavior.php b/classes/Kohana/ORM/Behavior.php new file mode 100644 index 0000000..cc837ba --- /dev/null +++ b/classes/Kohana/ORM/Behavior.php @@ -0,0 +1,44 @@ +_config = $config; + } + + public function on_construct($model, $id) { return TRUE; } + public function on_create($model) { } + public function on_update($model) { } +} diff --git a/classes/Kohana/ORM/Behavior/LocalBehavior.php b/classes/Kohana/ORM/Behavior/LocalBehavior.php new file mode 100644 index 0000000..a6c7897 --- /dev/null +++ b/classes/Kohana/ORM/Behavior/LocalBehavior.php @@ -0,0 +1,58 @@ +_callback = $callback; + } + + /** + * Constructs a new model and loads a record if given + * + *@param ORM $model The model + * @param mixed $id Parameter for find or object to load + */ + public function on_construct($model, $id) + { + $params = array('construct', $id); + $result = call_user_func_array($this->_callback, $params); + + if (is_bool($result)) + return $result; + + // Continue loading the record + return TRUE; + } + + /** + * The model is updated + */ + public function on_update($model) + { + $params = array('update'); + call_user_func_array($this->_callback, $params); + } + + /** + * A new model is created + * + * @param ORM $model The model + */ + public function on_create($model) + { + $params = array('create'); + call_user_func_array($this->_callback, $params); + } +} diff --git a/classes/ORM/Behavior.php b/classes/ORM/Behavior.php new file mode 100644 index 0000000..af04c99 --- /dev/null +++ b/classes/ORM/Behavior.php @@ -0,0 +1,3 @@ +_guid_column = Arr::get($config, 'column', $this->_guid_column); + $this->_guid_only = Arr::get($config, 'guid_only', $this->_guid_only); + } + + /** + * Constructs a new model and loads a record if given + * + * @param ORM $model The model + * @param mixed $id Parameter for find or object to load + */ + public function on_construct($model, $id) + { + if ($id !== NULL) + { + if (UUID::valid($id)) + { + $model->where($this->_guid_column, '=', $id)->find(); + } + + return TRUE; + } + + // Prevent further record loading + return FALSE; + } + + /** + * The model is updated, add a guid value if empty + * + * @param ORM $model The model + */ + public function on_update($model) + { + $this->create_guid($model); + } + + /** + * A new model is created, add a guid value + * + * @param ORM $model The model + */ + public function on_create($model) + { + $this->create_guid($model); + } + + private function create_guid($model) + { + $current_guid = $model->get($this->_guid_column); + + // Try to create a new GUID + $query = DB::select()->from($model->table_name()) + ->where($this->_guid_column, '=', ':guid') + ->limit(1); + + while (empty($current_guid)) + { + $current_guid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + + // 16 bits for "time_mid" + mt_rand(0, 0xffff), + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + + // 48 bits for "node" + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + + $query->param(':guid', $current_guid); + if ($query->execute()->get($model->primary_key(), FALSE) !== FALSE) + { + Log::instance()->add(Log::NOTICE, 'Duplicate GUID created for '.$model->table_name()); + $current_guid = ''; + } + } + + $model->set($this->_guid_column, $current_guid); + } +} diff --git a/classes/ORM/Behavior/LocalBehavior.php b/classes/ORM/Behavior/LocalBehavior.php new file mode 100644 index 0000000..41f2536 --- /dev/null +++ b/classes/ORM/Behavior/LocalBehavior.php @@ -0,0 +1,3 @@ +_slug_source_column = Arr::get($config, 'source', $this->_slug_source_column); + $this->_slug_column = Arr::get($config, 'column', $this->_slug_column); + } + + /** + * Constructs a new model and loads a record if given + * + * @param ORM $model The model + * @param mixed $id Parameter for find or object to load + */ + public function on_construct($model, $id) + { + if (($id !== NULL) AND ! is_array($id) AND ! ctype_digit($id)) + { + $model->where($this->_slug_column, '=', $id)->find(); + + return TRUE; + } + + // Prevent further record loading + return FALSE; + } + + /** + * The model is updated, add a slug value if empty + * + * @param ORM $model The model + */ + public function on_update($model) + { + $this->create_slug($model); + } + + /** + * A new model is created, add a slug value + * + * @param ORM $model The model + */ + public function on_create($model) + { + $this->create_slug($model); + } + + private function create_slug($model) + { + $index = 0; + $current_slug = $model->get($this->_slug_column); + + // Create a valid slug name + $source = $model->get($this->_slug_source_column); + if (empty($source)) + { + $source = $model->object_name(); + } + + // Prepare the query + $query = DB::select()->from($model->table_name()) + ->where($this->_slug_column, '=', ':slug') + ->where($model->primary_key(), '!=', $model->pk()) + ->limit(1); + + // Create a slugged value + $slug_base = $this->slugify($source); + + if ($current_slug !== $slug_base) + { + // Just the base slug + $current_slug = $slug_base; + $query->param(':slug', $current_slug); + + // Default slug invalid, add index + if ($query->execute()->get($model->primary_key(), FALSE) !== FALSE) + { + + // Base slug string with an index + do + { + $current_slug = sprintf('%s-%d', $slug_base, $index); + + $query->param(':slug', $current_slug); + if ($query->execute()->get($model->primary_key(), FALSE) !== FALSE) + { + $index++; + $current_slug = ''; + } + } + while (empty($current_slug)); + } + + $model->set($this->_slug_column, $current_slug); + } + } + + + /** + * Create a safe pathname + */ + protected function slugify($text, $strict=TRUE) + { + $text = $this->remove_accents($text); + + // replace non letter or digits by - + $text = preg_replace('~[^\\pL\d.]+~u', '-', $text); + + // trim + $text = trim($text, '-'); + + // lowercase + $text = strtolower($text); + + // remove unwanted characters + $text = preg_replace('~[^-\w.]+~', '', $text); + + if ($strict) + { + $text = str_replace('.', '', $text); + } + + return $text; + } + + /** + * Check if the input file looks like an utf8 string + */ + function seems_utf8($str) + { + $length = strlen($str); + for ($i=0; $i < $length; $i++) { + $c = ord($str[$i]); + if ($c < 0x80) { $n = 0; } // 0bbbbbbb + elseif (($c & 0xE0) == 0xC0) { $n=1; } // 110bbbbb + elseif (($c & 0xF0) == 0xE0) { $n=2; } // 1110bbbb + elseif (($c & 0xF8) == 0xF0) { $n=3; } // 11110bbb + elseif (($c & 0xFC) == 0xF8) { $n=4; } // 111110bb + elseif (($c & 0xFE) == 0xFC) { $n=5; } // 1111110b + else return FALSE; // Does not match any model + + for ($j=0; $j<$n; $j++) + { // n bytes matching 10bbbbbb follow ? + if ((++$i == $length) OR ((ord($str[$i]) & 0xC0) != 0x80)) + return FALSE; + } + } + return TRUE; + } + + /** + * Replace the accents + */ + function remove_accents($string) + { + if ( ! preg_match('/[\x80-\xff]/', $string)) + return $string; + + if ($this->seems_utf8($string)) { + $chars = array( + // Decompositions for Latin-1 Supplement + chr(195).chr(128) => 'A', chr(195).chr(129) => 'A', + chr(195).chr(130) => 'A', chr(195).chr(131) => 'A', + chr(195).chr(132) => 'A', chr(195).chr(133) => 'A', + chr(195).chr(134) => 'AE',chr(195).chr(135) => 'C', + chr(195).chr(136) => 'E', chr(195).chr(137) => 'E', + chr(195).chr(138) => 'E', chr(195).chr(139) => 'E', + chr(195).chr(140) => 'I', chr(195).chr(141) => 'I', + chr(195).chr(142) => 'I', chr(195).chr(143) => 'I', + chr(195).chr(144) => 'D', chr(195).chr(145) => 'N', + chr(195).chr(146) => 'O', chr(195).chr(147) => 'O', + chr(195).chr(148) => 'O', chr(195).chr(149) => 'O', + chr(195).chr(150) => 'O', chr(195).chr(153) => 'U', + chr(195).chr(154) => 'U', chr(195).chr(155) => 'U', + chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y', + chr(195).chr(158) => 'TH',chr(195).chr(159) => 's', + chr(195).chr(160) => 'a', chr(195).chr(161) => 'a', + chr(195).chr(162) => 'a', chr(195).chr(163) => 'a', + chr(195).chr(164) => 'a', chr(195).chr(165) => 'a', + chr(195).chr(166) => 'ae',chr(195).chr(167) => 'c', + chr(195).chr(168) => 'e', chr(195).chr(169) => 'e', + chr(195).chr(170) => 'e', chr(195).chr(171) => 'e', + chr(195).chr(172) => 'i', chr(195).chr(173) => 'i', + chr(195).chr(174) => 'i', chr(195).chr(175) => 'i', + chr(195).chr(176) => 'd', chr(195).chr(177) => 'n', + chr(195).chr(178) => 'o', chr(195).chr(179) => 'o', + chr(195).chr(180) => 'o', chr(195).chr(181) => 'o', + chr(195).chr(182) => 'o', chr(195).chr(184) => 'o', + chr(195).chr(185) => 'u', chr(195).chr(186) => 'u', + chr(195).chr(187) => 'u', chr(195).chr(188) => 'u', + chr(195).chr(189) => 'y', chr(195).chr(190) => 'th', + chr(195).chr(191) => 'y', + // Decompositions for Latin Extended-A + chr(196).chr(128) => 'A', chr(196).chr(129) => 'a', + chr(196).chr(130) => 'A', chr(196).chr(131) => 'a', + chr(196).chr(132) => 'A', chr(196).chr(133) => 'a', + chr(196).chr(134) => 'C', chr(196).chr(135) => 'c', + chr(196).chr(136) => 'C', chr(196).chr(137) => 'c', + chr(196).chr(138) => 'C', chr(196).chr(139) => 'c', + chr(196).chr(140) => 'C', chr(196).chr(141) => 'c', + chr(196).chr(142) => 'D', chr(196).chr(143) => 'd', + chr(196).chr(144) => 'D', chr(196).chr(145) => 'd', + chr(196).chr(146) => 'E', chr(196).chr(147) => 'e', + chr(196).chr(148) => 'E', chr(196).chr(149) => 'e', + chr(196).chr(150) => 'E', chr(196).chr(151) => 'e', + chr(196).chr(152) => 'E', chr(196).chr(153) => 'e', + chr(196).chr(154) => 'E', chr(196).chr(155) => 'e', + chr(196).chr(156) => 'G', chr(196).chr(157) => 'g', + chr(196).chr(158) => 'G', chr(196).chr(159) => 'g', + chr(196).chr(160) => 'G', chr(196).chr(161) => 'g', + chr(196).chr(162) => 'G', chr(196).chr(163) => 'g', + chr(196).chr(164) => 'H', chr(196).chr(165) => 'h', + chr(196).chr(166) => 'H', chr(196).chr(167) => 'h', + chr(196).chr(168) => 'I', chr(196).chr(169) => 'i', + chr(196).chr(170) => 'I', chr(196).chr(171) => 'i', + chr(196).chr(172) => 'I', chr(196).chr(173) => 'i', + chr(196).chr(174) => 'I', chr(196).chr(175) => 'i', + chr(196).chr(176) => 'I', chr(196).chr(177) => 'i', + chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij', + chr(196).chr(180) => 'J', chr(196).chr(181) => 'j', + chr(196).chr(182) => 'K', chr(196).chr(183) => 'k', + chr(196).chr(184) => 'k', chr(196).chr(185) => 'L', + chr(196).chr(186) => 'l', chr(196).chr(187) => 'L', + chr(196).chr(188) => 'l', chr(196).chr(189) => 'L', + chr(196).chr(190) => 'l', chr(196).chr(191) => 'L', + chr(197).chr(128) => 'l', chr(197).chr(129) => 'L', + chr(197).chr(130) => 'l', chr(197).chr(131) => 'N', + chr(197).chr(132) => 'n', chr(197).chr(133) => 'N', + chr(197).chr(134) => 'n', chr(197).chr(135) => 'N', + chr(197).chr(136) => 'n', chr(197).chr(137) => 'N', + chr(197).chr(138) => 'n', chr(197).chr(139) => 'N', + chr(197).chr(140) => 'O', chr(197).chr(141) => 'o', + chr(197).chr(142) => 'O', chr(197).chr(143) => 'o', + chr(197).chr(144) => 'O', chr(197).chr(145) => 'o', + chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe', + chr(197).chr(148) => 'R',chr(197).chr(149) => 'r', + chr(197).chr(150) => 'R',chr(197).chr(151) => 'r', + chr(197).chr(152) => 'R',chr(197).chr(153) => 'r', + chr(197).chr(154) => 'S',chr(197).chr(155) => 's', + chr(197).chr(156) => 'S',chr(197).chr(157) => 's', + chr(197).chr(158) => 'S',chr(197).chr(159) => 's', + chr(197).chr(160) => 'S', chr(197).chr(161) => 's', + chr(197).chr(162) => 'T', chr(197).chr(163) => 't', + chr(197).chr(164) => 'T', chr(197).chr(165) => 't', + chr(197).chr(166) => 'T', chr(197).chr(167) => 't', + chr(197).chr(168) => 'U', chr(197).chr(169) => 'u', + chr(197).chr(170) => 'U', chr(197).chr(171) => 'u', + chr(197).chr(172) => 'U', chr(197).chr(173) => 'u', + chr(197).chr(174) => 'U', chr(197).chr(175) => 'u', + chr(197).chr(176) => 'U', chr(197).chr(177) => 'u', + chr(197).chr(178) => 'U', chr(197).chr(179) => 'u', + chr(197).chr(180) => 'W', chr(197).chr(181) => 'w', + chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y', + chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z', + chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z', + chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z', + chr(197).chr(190) => 'z', chr(197).chr(191) => 's', + // Decompositions for Latin Extended-B + chr(200).chr(152) => 'S', chr(200).chr(153) => 's', + chr(200).chr(154) => 'T', chr(200).chr(155) => 't', + // Euro Sign + chr(226).chr(130).chr(172) => 'E', + // GBP (Pound) Sign + chr(194).chr(163) => ''); + + $string = strtr($string, $chars); + } else { + // Assume ISO-8859-1 if not UTF-8 + $chars['in'] = chr(128).chr(131).chr(138).chr(142).chr(154).chr(158) + .chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194) + .chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202) + .chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210) + .chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218) + .chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227) + .chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235) + .chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243) + .chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251) + .chr(252).chr(253).chr(255); + + $chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy"; + + $string = strtr($string, $chars['in'], $chars['out']); + $double_chars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254)); + $double_chars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th'); + $string = str_replace($double_chars['in'], $double_chars['out'], $string); + } + + return $string; + } +} From a4a096e0a284e88a1025df4b7f68d3f19c9c2bf4 Mon Sep 17 00:00:00 2001 From: Sven Rymenants Date: Tue, 5 Apr 2016 21:32:45 +0200 Subject: [PATCH 3/3] Default behavior function --- classes/Kohana/ORM.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/classes/Kohana/ORM.php b/classes/Kohana/ORM.php index 1cce472..ac47160 100644 --- a/classes/Kohana/ORM.php +++ b/classes/Kohana/ORM.php @@ -1359,6 +1359,16 @@ public function labels() return array(); } + /** + * Behavior definition for the model + * + * @return array + */ + public function behaviors() + { + return array(); + } + /** * Validates the current model's data *