From fb2c49876b4f55d087662eb333d37569a9586e96 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 5 May 2020 21:45:42 -0500 Subject: [PATCH 1/7] Introduce set_defaults() method. This commit introduces a private data() array, and a public method for classes that extend Base to call to setup the defaults. This is necessary to make sure that all default class variables make their way into the data array so that magic methods can access them like they can any other variable. --- base.php | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 129 insertions(+), 11 deletions(-) diff --git a/base.php b/base.php index a447da2..3fe6ee1 100644 --- a/base.php +++ b/base.php @@ -24,6 +24,15 @@ */ class Base { + /** + * The private data array used to store class attributes, so that magic + * methods work as intended. + * + * @since 1.1.0 + * @var array + */ + private $data = array(); + /** * The name of the PHP global that contains the primary database interface. * @@ -60,7 +69,7 @@ class Base { /** Public ****************************************************************/ /** - * Magic isset'ter for immutability. + * Magic issetter, for immutability. * * @since 1.0.0 * @@ -74,14 +83,23 @@ public function __isset( $key = '' ) { $key = 'id'; } + // Bail if empty key + if ( empty( $key ) ) { + return false; + } + // Class method to try and call $method = "get_{$key}"; - // Return property if exists - if ( method_exists( $this, $method ) ) { + // Return true if method exists + if ( is_callable( $this, $method ) ) { + return true; + + // Return true if data exists + } elseif ( isset( $this->data[ $key ] ) ) { return true; - // Return get method results if exists + // Return true if property exists } elseif ( property_exists( $this, $key ) ) { return true; } @@ -90,6 +108,60 @@ public function __isset( $key = '' ) { return false; } + /** + * Magic unsetter, for immutability. + * + * @since 1.1.0 + * + * @param string $key User meta key to unset. + */ + public function __unset( $key = '' ) { + + // No more uppercase ID properties ever + if ( 'ID' === $key ) { + $key = 'id'; + } + + // Bail if empty key + if ( empty( $key ) ) { + return false; + } + + // Maybe unset from data array + if ( isset( $this->data[ $key ] ) ) { + unset( $this->data[ $key ] ); + } + + // Maybe unset property + if ( property_exists( $this, $key ) ) { + unset( $this->{$key} ); + } + } + + /** + * Magic setter, for immutability. + * + * @since 1.1.0 + * + * @param string $key + * @return mixed + */ + public function __set( $key = '', $value = '' ) { + + // No more uppercase ID properties ever + if ( 'ID' === $key ) { + $key = 'id'; + } + + // Bail if empty key + if ( empty( $key ) ) { + return false; + } + + // Set the data value by key + $this->data[ $key ] = $value; + } + /** * Magic getter for immutability. * @@ -98,27 +170,39 @@ public function __isset( $key = '' ) { * @param string $key * @return mixed */ - public function __get( $key = '' ) { + public function &__get( $key = '' ) { // No more uppercase ID properties ever if ( 'ID' === $key ) { $key = 'id'; } + // Bail if empty key + if ( empty( $key ) ) { + return null; + } + // Class method to try and call $method = "get_{$key}"; - // Return property if exists - if ( method_exists( $this, $method ) ) { + // Return from method if exists + if ( is_callable( $this, $method ) ) { return call_user_func( array( $this, $method ) ); - // Return get method results if exists + // Return from data array if set + } elseif ( isset( $this->data[ $key ] ) ) { + return $this->data[ $key ]; + + // Return from property if set } elseif ( property_exists( $this, $key ) ) { return $this->{$key}; } + // Set to null + $this->data[ $key ] = null; + // Return null if not exists - return null; + return $this->data[ $key ]; } /** @@ -129,7 +213,37 @@ public function __get( $key = '' ) { * @return array Array version of the given object. */ public function to_array() { - return get_object_vars( $this ); + return $this->data; + } + + /** + * Get this objects default properties and set them up in the private data + * array. Use this in the Constructor in any class that extends this class. + * + * @since 1.1.0 + */ + public function set_defaults() { + + // Get the hard-coded object variables + $r = get_object_vars( $this ); + + // Set those vars + $this->set_vars( $r ); + + // Data is private and protected + unset( $r['data'] ); + + // Maybe cleanup + if ( ! empty( $r ) ) { + + // Get keys + $keys = array_keys( $r ); + + // Cleanup class properties + foreach ( $keys as $key ) { + unset( $this->{$key} ); + } + } } /** Protected *************************************************************/ @@ -263,7 +377,11 @@ protected function set_vars( $args = array() ) { // Set all properties foreach ( $args as $key => $value ) { - $this->{$key} = $value; + + // Avoid recusion + if ( 'data' !== $key ) { + $this->data[ $key ] = $value; + } } } From ad0ebf40b7f60d3e80a91cf355d6d4675730f5c1 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 5 May 2020 21:47:57 -0500 Subject: [PATCH 2/7] Use set_defaults() in all other classes. This commit is necessary to make sure that all object properties are correctly setup in the private data array. --- column.php | 3 +++ query.php | 1 + row.php | 5 +++++ schema.php | 3 +++ table.php | 3 +++ 5 files changed, 15 insertions(+) diff --git a/column.php b/column.php index 9d8b59e..238f7bd 100644 --- a/column.php +++ b/column.php @@ -409,6 +409,9 @@ class Column extends Base { */ public function __construct( $args = array() ) { + // Setup the defaults + $this->set_defaults(); + // Parse arguments $r = $this->parse_args( $args ); diff --git a/query.php b/query.php index 58f1343..2c05acc 100644 --- a/query.php +++ b/query.php @@ -321,6 +321,7 @@ class Query extends Base { public function __construct( $query = array() ) { // Setup + $this->set_defaults(); $this->set_alias(); $this->set_prefix(); $this->set_columns(); diff --git a/row.php b/row.php index ddd54ea..cb4dc97 100644 --- a/row.php +++ b/row.php @@ -36,6 +36,11 @@ class Row extends Base { * @param mixed Null by default, Array/Object if not */ public function __construct( $item = null ) { + + // Setup the defaults + $this->set_defaults(); + + // Maybe initialize if ( ! empty( $item ) ) { $this->init( $item ); } diff --git a/schema.php b/schema.php index ca112cb..da83998 100644 --- a/schema.php +++ b/schema.php @@ -44,6 +44,9 @@ public function __construct() { return; } + // Setup the defaults + $this->set_defaults(); + // Juggle original columns array $columns = $this->columns; $this->columns = array(); diff --git a/table.php b/table.php index 9d7ad72..87e9ae8 100644 --- a/table.php +++ b/table.php @@ -153,6 +153,9 @@ public function __construct() { return; } + // Setup the defaults + $this->set_defaults(); + // Add the table to the database interface $this->set_db_interface(); From 30c9a5a6298a4b0498a65bfb4478f6fff9f7eb17 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 25 Aug 2020 21:27:43 -0500 Subject: [PATCH 3/7] Schema: Set defaults earlier, and introduce set_columns(). --- schema.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/schema.php b/schema.php index 60a5a1d..e686ae2 100644 --- a/schema.php +++ b/schema.php @@ -39,14 +39,26 @@ class Schema extends Base { */ public function __construct() { + // Setup the defaults + $this->set_defaults(); + + // Setup the columns + $this->set_columns(); + } + + /** + * Setup the columns. + * + * @since 1.0.0 + * @return void + */ + public function set_columns() { + // Bail if no columns if ( empty( $this->columns ) || ! is_array( $this->columns ) ) { return; } - // Setup the defaults - $this->set_defaults(); - // Juggle original columns array $columns = $this->columns; $this->columns = array(); From c33f90f21d83b16b44aa6c6f535c847201151658 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 25 Aug 2020 21:30:33 -0500 Subject: [PATCH 4/7] Date Query: Call set_defaults() immediately. --- date.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/date.php b/date.php index 70a5581..78f52f5 100644 --- a/date.php +++ b/date.php @@ -220,6 +220,9 @@ class Date extends Base { */ public function __construct( $date_query = array() ) { + // Setup the defaults + $this->set_defaults(); + // Bail if not an array. if ( ! is_array( $date_query ) ) { return; From 6e93309f90dce746b2050234e1c19df10f815269 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Tue, 25 Aug 2020 21:48:03 -0500 Subject: [PATCH 5/7] Better explain why setting value to null is necessary. --- base.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base.php b/base.php index 026270a..47cc6aa 100644 --- a/base.php +++ b/base.php @@ -198,11 +198,11 @@ public function &__get( $key = '' ) { return $this->{$key}; } - // Set to null + // Set key to null, so array operations work correctly $this->data[ $key ] = null; - // Return null if not exists - return $this->data[ $key ]; + // Return null + return null; } /** From fdcc5beb4553c683c4680cfe3dc85dcc6701b014 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Sat, 19 Sep 2020 23:14:18 -0500 Subject: [PATCH 6/7] &__get() must return a byref value. --- base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base.php b/base.php index 47cc6aa..3f2762f 100644 --- a/base.php +++ b/base.php @@ -202,7 +202,7 @@ public function &__get( $key = '' ) { $this->data[ $key ] = null; // Return null - return null; + return $this->data[ $key ]; } /** From 72389317aee6e3f5fccf33d05ec5c347637c77f6 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Mon, 21 Sep 2020 20:18:15 -0500 Subject: [PATCH 7/7] Various fixes to make $data storage work correctly * Change from array to object * Introduce unset_vars() to parlay off of set_vars() * Introduce validate_key() to reduce some code duplication in magic methods * Switch from is_callable() to method_exists() so that it works as intended with __call() * Make sure magic un/set methods operate on the correct variables * Avoid setting object array keys directly, triggering byref debug notices * Some general code & docs improvements to related methods * Add 'join' to query & request clauses --- base.php | 203 +++++++++++++++++++++++++++++++++------------------- column.php | 4 +- compare.php | 2 +- date.php | 5 +- query.php | 36 +++++++--- schema.php | 11 +-- 6 files changed, 167 insertions(+), 94 deletions(-) diff --git a/base.php b/base.php index 3f2762f..16982e5 100644 --- a/base.php +++ b/base.php @@ -25,13 +25,13 @@ class Base { /** - * The private data array used to store class attributes, so that magic + * The private data object used to store class attributes, so that magic * methods work as intended. * * @since 1.1.0 - * @var array + * @var object */ - private $data = array(); + private $data; /** * The name of the PHP global that contains the primary database interface. @@ -69,7 +69,7 @@ class Base { /** Public ****************************************************************/ /** - * Magic issetter, for immutability. + * Magic isset method. * * @since 1.0.0 * @@ -78,29 +78,23 @@ class Base { */ public function __isset( $key = '' ) { - // No more uppercase ID properties ever - if ( 'ID' === $key ) { - $key = 'id'; - } + // Validate the key + $key = $this->validate_key( $key ); - // Bail if empty key + // Bail if invalid key if ( empty( $key ) ) { return false; } // Class method to try and call - $method = "get_{$key}"; + $method = "__get_{$key}"; // Return true if method exists - if ( is_callable( $this, $method ) ) { + if ( method_exists( $this, $method ) ) { return true; // Return true if data exists - } elseif ( isset( $this->data[ $key ] ) ) { - return true; - - // Return true if property exists - } elseif ( property_exists( $this, $key ) ) { + } elseif ( isset( $this->data->{$key} ) ) { return true; } @@ -109,7 +103,7 @@ public function __isset( $key = '' ) { } /** - * Magic unsetter, for immutability. + * Magic unset method. * * @since 1.1.0 * @@ -117,29 +111,22 @@ public function __isset( $key = '' ) { */ public function __unset( $key = '' ) { - // No more uppercase ID properties ever - if ( 'ID' === $key ) { - $key = 'id'; - } + // Validate the key + $key = $this->validate_key( $key ); - // Bail if empty key + // Bail if invalid key if ( empty( $key ) ) { return false; } // Maybe unset from data array - if ( isset( $this->data[ $key ] ) ) { - unset( $this->data[ $key ] ); - } - - // Maybe unset property - if ( property_exists( $this, $key ) ) { - unset( $this->{$key} ); + if ( isset( $this->data->{$key} ) ) { + unset( $this->data->{$key} ); } } /** - * Magic setter, for immutability. + * Magic set method. * * @since 1.1.0 * @@ -148,22 +135,28 @@ public function __unset( $key = '' ) { */ public function __set( $key = '', $value = '' ) { - // No more uppercase ID properties ever - if ( 'ID' === $key ) { - $key = 'id'; - } + // Validate the key + $key = $this->validate_key( $key ); - // Bail if empty key + // Bail if invalid key if ( empty( $key ) ) { return false; } - // Set the data value by key - $this->data[ $key ] = $value; + // Class method to try and call + $method = "__set_{$key}"; + + // Maybe override the value + if ( method_exists( $this, $method ) ) { + $value = call_user_func( array( $this, $method ), $value ); + } + + // Set the key to the value + $this->data->{$key} = $value; } /** - * Magic getter for immutability. + * Magic get method. * * @since 1.0.0 * @@ -172,37 +165,39 @@ public function __set( $key = '', $value = '' ) { */ public function &__get( $key = '' ) { - // No more uppercase ID properties ever - if ( 'ID' === $key ) { - $key = 'id'; - } + // Validate the key + $key = $this->validate_key( $key ); - // Bail if empty key + // Bail if invalid key if ( empty( $key ) ) { - return null; + return false; } + // Default return value + $retval = null; + // Class method to try and call - $method = "get_{$key}"; + $method = "__get_{$key}"; // Return from method if exists - if ( is_callable( $this, $method ) ) { - return call_user_func( array( $this, $method ) ); + if ( method_exists( $this, $method ) ) { + $retval = call_user_func( array( $this, $method ) ); // Return from data array if set - } elseif ( isset( $this->data[ $key ] ) ) { - return $this->data[ $key ]; + } elseif ( isset( $this->data->{$key} ) ) { + $retval = $this->data->{$key}; + } - // Return from property if set - } elseif ( property_exists( $this, $key ) ) { - return $this->{$key}; + // Return if not null + if ( ! is_null( $retval ) ) { + return $retval; } // Set key to null, so array operations work correctly - $this->data[ $key ] = null; + $this->data->{$key} = $retval; - // Return null - return $this->data[ $key ]; + // Return variable byref + return $this->data->{$key}; } /** @@ -213,7 +208,7 @@ public function &__get( $key = '' ) { * @return array Array version of the given object. */ public function to_array() { - return $this->data; + return get_object_vars( $this->data ); } /** @@ -227,23 +222,14 @@ public function set_defaults() { // Get the hard-coded object variables $r = get_object_vars( $this ); - // Set those vars - $this->set_vars( $r ); - - // Data is private and protected + // Data is private, so don't set it recursively unset( $r['data'] ); - // Maybe cleanup - if ( ! empty( $r ) ) { - - // Get keys - $keys = array_keys( $r ); + // Set those vars + $this->set_vars( $r ); - // Cleanup class properties - foreach ( $keys as $key ) { - unset( $this->{$key} ); - } - } + // Unset those vars + $this->unset_vars( $r ); } /** Protected *************************************************************/ @@ -360,6 +346,11 @@ protected function sanitize_table_name( $name = '' ) { /** * Set class variables from arguments. * + * This method accepts a key/value array of class variables to set, and is + * used by set_defaults() to prepare a class for magic property overrides. + * + * It can also be called directly to set multiple class variables. + * * @since 1.0.0 * @param array $args */ @@ -375,13 +366,44 @@ protected function set_vars( $args = array() ) { $args = (array) $args; } + // Set empty class + $this->data = new \stdClass(); + // Set all properties foreach ( $args as $key => $value ) { + $this->data->{$key} = $value; + } + } - // Avoid recusion - if ( 'data' !== $key ) { - $this->data[ $key ] = $value; - } + /** + * Unset class variables from arguments. + * + * This method accepts a key/value array of class variables to unset, and is + * used by set_defaults() to prepare a class for magic property overrides. + * + * It can also be called directly to unset multiple class variables. + * + * @since 1.0.0 + * @param array $args + */ + protected function unset_vars( $args = array() ) { + + // Bail if no vars to clean + if ( empty( $args ) ) { + return; + } + + // Cast to an array + if ( ! is_array( $args ) ) { + $args = (array) $args; + } + + // Get keys + $keys = array_keys( $args ); + + // Cleanup class properties + foreach ( $keys as $key ) { + unset( $this->{$key} ); } } @@ -400,7 +422,7 @@ protected function get_db() { $retval = false; // Look for a commonly used global database interface - if ( isset( $GLOBALS[ $this->db_global ] ) ) { + if ( ! empty( $this->db_global ) && isset( $GLOBALS[ $this->db_global ] ) ) { $retval = $GLOBALS[ $this->db_global ]; } @@ -452,4 +474,35 @@ protected function is_success( $result = false ) { // Return the result return (bool) $retval; } + + /** Private ***************************************************************/ + + /** + * Validate a data key. + * + * @since 1.0.0 + * + * @param string $key + * @return boolean|string + */ + private function validate_key( $key = '' ) { + + // Bail if empty key + if ( empty( $key ) ) { + return false; + } + + // Bail if setting data + if ( 'data' === $key ) { + return false; + } + + // No more uppercase ID properties ever + if ( 'ID' === $key ) { + return 'id'; + } + + // Return the original key + return $key; + } } diff --git a/column.php b/column.php index 3d7ec1f..9dd0bd9 100644 --- a/column.php +++ b/column.php @@ -541,7 +541,7 @@ private function validate_args( $args = array() ) { foreach ( $args as $key => $value ) { // Callback is callable - if ( isset( $callbacks[ $key ] ) && is_callable( $callbacks[ $key ] ) ) { + if ( ! empty( $callbacks[ $key ] ) && is_callable( $callbacks[ $key ] ) ) { $r[ $key ] = call_user_func( $callbacks[ $key ], $value ); // Callback is malformed so just let it through to avoid breakage @@ -696,7 +696,7 @@ private function sanitize_pattern( $pattern = false ) { private function sanitize_validation( $callback = '' ) { // Return callback if it's callable - if ( is_callable( $callback ) ) { + if ( ! empty( $callback ) && is_callable( $callback ) ) { return $callback; } diff --git a/compare.php b/compare.php index 9223e6f..7593554 100644 --- a/compare.php +++ b/compare.php @@ -154,4 +154,4 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) return $sql_chunks; } -} \ No newline at end of file +} diff --git a/date.php b/date.php index 78f52f5..ed449f5 100644 --- a/date.php +++ b/date.php @@ -1162,7 +1162,10 @@ public function build_time_query( $column, $compare, $hour = null, $minute = nul // Build the SQL $query = "DATE_FORMAT( {$column}, %s ) {$compare} %f"; + // Get the return value + $retval = $this->get_db()->prepare( $query, $format, $time ); + // Return the prepared SQL - return $this->get_db()->prepare( $query, $format, $time ); + return $retval; } } diff --git a/query.php b/query.php index 163d1bc..9e0c330 100644 --- a/query.php +++ b/query.php @@ -158,6 +158,7 @@ class Query extends Base { protected $query_clauses = array( 'select' => '', 'from' => '', + 'join' => array(), 'where' => array(), 'groupby' => '', 'orderby' => '', @@ -173,6 +174,7 @@ class Query extends Base { protected $request_clauses = array( 'select' => '', 'from' => '', + 'join' => '', 'where' => '', 'groupby' => '', 'orderby' => '', @@ -460,32 +462,41 @@ private function set_query_var_defaults() { return; } + // Default defaults + $defaults = array(); + // Direct column names $names = wp_list_pluck( $this->columns, 'name' ); foreach ( $names as $name ) { - $this->query_var_defaults[ $name ] = $this->query_var_default_value; + $defaults[ $name ] = $this->query_var_default_value; } // Possible ins $possible_ins = $this->get_columns( array( 'in' => true ), 'and', 'name' ); foreach ( $possible_ins as $in ) { $key = "{$in}__in"; - $this->query_var_defaults[ $key ] = false; + $defaults[ $key ] = false; } // Possible not ins $possible_not_ins = $this->get_columns( array( 'not_in' => true ), 'and', 'name' ); foreach ( $possible_not_ins as $in ) { $key = "{$in}__not_in"; - $this->query_var_defaults[ $key ] = false; + $defaults[ $key ] = false; } // Possible dates $possible_dates = $this->get_columns( array( 'date_query' => true ), 'and', 'name' ); foreach ( $possible_dates as $date ) { $key = "{$date}_query"; - $this->query_var_defaults[ $key ] = false; + $defaults[ $key ] = false; } + + // Set the default query variables + $this->query_var_defaults = array_merge( + $this->query_var_defaults, + $defaults + ); } /** @@ -535,15 +546,18 @@ private function set_request_clauses( $clauses = array() ) { // Select & From $table = $this->get_table_name(); $select = "SELECT {$found_rows} {$fields}"; - $from = "FROM {$table} {$this->table_alias} {$join}"; + $from = "FROM {$table} {$this->table_alias}"; // Put query into clauses array - $this->request_clauses['select'] = $select; - $this->request_clauses['from'] = $from; - $this->request_clauses['where'] = $where; - $this->request_clauses['groupby'] = $groupby; - $this->request_clauses['orderby'] = $orderby; - $this->request_clauses['limits'] = $limits; + $this->request_clauses = array( + 'select' => $select, + 'from' => $from, + 'join' => $join, + 'where' => $where, + 'groupby' => $groupby, + 'orderby' => $orderby, + 'limits' => $limits + ); } /** diff --git a/schema.php b/schema.php index e686ae2..16e92c3 100644 --- a/schema.php +++ b/schema.php @@ -50,7 +50,6 @@ public function __construct() { * Setup the columns. * * @since 1.0.0 - * @return void */ public function set_columns() { @@ -61,16 +60,20 @@ public function set_columns() { // Juggle original columns array $columns = $this->columns; - $this->columns = array(); + $this->columns = $new_columns = array(); // Loop through columns and create objects from them foreach ( $columns as $column ) { if ( is_array( $column ) ) { - $this->columns[] = new Column( $column ); + $new_columns[] = new Column( $column ); + } elseif ( $column instanceof Column ) { - $this->columns[] = $column; + $new_columns[] = $column; } } + + // Set the columns + $this->columns = $new_columns; } /**