From 9c30c3671c34422bed2ceba5c7a513bef91192f5 Mon Sep 17 00:00:00 2001 From: gdarko Date: Sat, 19 Aug 2023 07:41:36 +0300 Subject: [PATCH] Version v1.1.0 --- README.md | 13 +- composer.json | 13 +- src/Lib/functions.php => functions.php | 5 +- src/Abstracts/DataModel.php | 788 +++++--- src/Collection.php | 144 ++ src/Contracts/Arrayable.php | 25 + src/Contracts/JSONable.php | 26 + src/Contracts/Stringable.php | 21 + src/QueryBuilder.php | 2032 +++++++++++--------- src/Traits/DataModelTrait.php | 175 -- tests/cases/AbstractModelTest.php | 1 + tests/cases/FunctionsTest.php | 3 +- tests/cases/HooksTest.php | 1 + tests/cases/QueryBuilderConditionsTest.php | 3 +- tests/cases/QueryBuilderOperationsTest.php | 3 +- tests/cases/QueryBuilderStatementsTest.php | 3 +- tests/cases/SanitizationTest.php | 3 +- tests/cases/TraitModelTest.php | 1 + tests/framework/class.model.php | 4 +- 19 files changed, 1931 insertions(+), 1333 deletions(-) rename src/Lib/functions.php => functions.php (76%) create mode 100644 src/Collection.php create mode 100644 src/Contracts/Arrayable.php create mode 100644 src/Contracts/JSONable.php create mode 100644 src/Contracts/Stringable.php delete mode 100644 src/Traits/DataModelTrait.php diff --git a/README.md b/README.md index 929eadb..e07ee53 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Wordpress Query Builder Library +# WordPress Query Builder Library [![Latest Stable Version](https://poser.pugx.org/10quality/wp-query-builder/v/stable)](https://packagist.org/packages/10quality/wp-query-builder) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/10quality/wp-query-builder/test.yml) @@ -7,21 +7,21 @@ This package provides a SQL query builder class built on top of WordPress core Database accessor. Usability is similar to Laravel's Eloquent. -The library also provides an abstract class and a trait to be used on data models built for custom tables. The abstract class extends our generic [PHP model](https://github.com/10quality/php-data-model) class. +The library also provides an abstract class and a trait to be used on data models built for custom tables. -This is the perfect package to use within the [WordPress MVC](https://www.wordpress-mvc.com/) framework. +This package is inspired by the [WordPress MVC's](https://www.wordpress-mvc.com/) Query Builder. ## Install This package / library requires composer. ```bash -composer require 10quality/wp-query-builder +composer require ignitekit/wp-query-builder ``` ## Usage & Documentation -Please read the [wiki](https://github.com/10quality/wp-query-builder/wiki) for documentation. +Please read the [wiki](https://github.com/ignitekit/wp-query-builder/wiki) for documentation. Quick snippet sample: ```php @@ -44,4 +44,5 @@ PSR-2 coding guidelines. ## License -MIT License (c) 2019 [10 Quality](https://www.10quality.com/). \ No newline at end of file +MIT License (c) 2019 [10 Quality](https://www.10quality.com/). +MIT License (c) 2023 [Darko G](https://darkog.com/). \ No newline at end of file diff --git a/composer.json b/composer.json index 5d676d8..9cc8c54 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "10quality/wp-query-builder", + "name": "ignitekit/wp-query-builder", "description": "Wordpress Query Builder class library for custom models and data querying.", "license": "MIT", "keywords": ["wordpress","query builder","database", "models"], @@ -15,21 +15,24 @@ { "name": "10 Quality", "email": "info@10quality.com" + }, + { + "name": "Darko G.", + "email": "dg@darkog.com" } ], "require": { - "php": ">=5.4", - "10quality/php-data-model": "^1.0" + "php": ">=5.4" }, "require-dev": { "phpunit/phpunit": "9.*" }, "autoload": { "psr-4": { - "TenQuality\\WP\\Database\\": "src" + "IgniteKit\\WP\\QueryBuilder\\": "src" }, "files": [ - "src/Lib/functions.php" + "functions.php" ] }, "scripts": { diff --git a/src/Lib/functions.php b/functions.php similarity index 76% rename from src/Lib/functions.php rename to functions.php index 0d8b287..3fb9b32 100644 --- a/src/Lib/functions.php +++ b/functions.php @@ -1,10 +1,11 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.9 @@ -16,7 +17,7 @@ * * @param string|null $query_id * - * @return \TenQuality\WP\Database\QueryBuilder + * @return \IgniteKit\WP\QueryBuilder\QueryBuilder */ function wp_query_builder( $query_id = null ) { diff --git a/src/Abstracts/DataModel.php b/src/Abstracts/DataModel.php index b409706..d778623 100644 --- a/src/Abstracts/DataModel.php +++ b/src/Abstracts/DataModel.php @@ -1,230 +1,584 @@ + * @copyright 10 Quality + * @copyright Darko G * @license MIT * @package wp-query-builder * @version 1.0.12 */ -abstract class DataModel extends Model -{ - /** - * Database table name. - * @since 1.0.0 - * @var string - */ - protected $table = ''; - /** - * Reference to primary key column name. - * @since 1.0.0 - * @var string - */ - protected $primary_key = 'ID'; - /** - * List of properties used for keyword search. - * @since 1.0.0 - * @var array - */ - protected static $keywords = []; - /** - * Default model constructor. - * @since 1.0.0 - * - * @param array $attributes - * @param mixed $id - */ - public function __construct( $attributes = [], $id = null ) - { - parent::__construct( $attributes ); - if ( ! empty( $id ) ) - $this->attributes[$this->primary_key] = $id; - } - /** - * Returns `tablename` property. - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @return string - */ - protected function getTablenameAlias() - { - global $wpdb; - return $wpdb->prefix . $this->table; - } - /** - * Returns list of protected/readonly properties for - * when saving or updating. - * @since 1.0.0 - * - * @return array - */ - protected function protected_properties() - { - return apply_filters( - 'data_model_' . $this->table . '_excluded_save_fields', - [$this->primary_key, 'created_at', 'updated_at'], - $this->tablename - ); - } - /** - * Saves data attributes in database. - * Returns flag indicating if save process was successful. - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @param bool $force_insert Flag that indicates if should insert regardless of ID. - * - * @return bool - */ - public function save( $force_insert = false ) - { - global $wpdb; - $protected = $this->protected_properties(); - if ( ! $force_insert && $this->{$this->primary_key} ) { - // Update - $success = $wpdb->update( $this->tablename, array_filter( $this->attributes, function( $key ) use( $protected ) { - return ! in_array( $key , $protected ); - }, ARRAY_FILTER_USE_KEY ), [$this->primary_key => $this->attributes[$this->primary_key]] ); - if ( $success ) - do_action( 'data_model_' . $this->table . '_updated', $this ); - } else { - // Insert - $success = $wpdb->insert( $this->tablename, array_filter( $this->attributes, function( $key ) use( $protected ) { - return ! in_array( $key , $protected ); - }, ARRAY_FILTER_USE_KEY ) ); - $this->{$this->primary_key} = $wpdb->insert_id; - $date = date( 'Y-m-d H:i:s' ); - $this->created_at = $date; - $this->updated_at = $date; - if ( $success ) - do_action( 'data_model_' . $this->table . '_inserted', $this ); - } - if ( $success ) - do_action( 'data_model_' . $this->table . '_save', $this ); - return $success; - } - /** - * Loads attributes from database. - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @return \TenQuality\WP\Database\Abstracts\DataModel|null - */ - public function load() - { - $builder = new QueryBuilder( $this->table . '_load' ); - $this->attributes = $builder->select( '*' ) - ->from( $this->table ) - ->where( [$this->primary_key => $this->attributes[$this->primary_key]] ) - ->first( ARRAY_A ); - return ! empty( $this->attributes ) - ? apply_filters( 'data_model_' . $this->table, $this ) - : null; - } - /** - * Loads attributes from database based on custome where statements - * - * Samples: - * // Simple query - * $this->load_where( ['slug' => 'this-example-1'] ); - * // Compound query with OR statement - * $this->load_where( ['ID' => 77, 'ID' => ['OR', 546]] ); - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @param array $args Query arguments. - * - * @return \TenQuality\WP\Database\Abstracts\DataModel|null - */ - public function load_where( $args ) - { - if ( empty( $args ) ) - return null; - if ( ! is_array( $args ) ) - throw new Exception( 'Arguments parameter must be an array.', 10100 ); - $builder = new QueryBuilder( $this->table . '_load_where' ); - $this->attributes = $builder->select( '*' ) - ->from( $this->table ) - ->where( $args ) - ->first( ARRAY_A ); - return ! empty( $this->attributes ) - ? apply_filters( 'data_model_' . $this->table, $this ) - : null; - } - /** - * Deletes record. - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @return bool - */ - public function delete() - { - global $wpdb; - $deleted = $this->{$this->primary_key} - ? $wpdb->delete( $this->tablename, [$this->primary_key => $this->attributes[$this->primary_key]] ) - : false; - if ( $deleted ) - do_action( 'data_model_' . $this->table . '_deleted', $this ); - return $deleted; - } - /** - * Updates specific columns of the model (not the whole object like save()). - * @since 1.0.12 - * - * @param array $data Data to update. - * - * @return bool - */ - public function update( $data = [] ) - { - // If not data, let save() handle this - if ( empty( $data ) || !is_array( $data ) ) { - return $this->save(); - } - global $wpdb; - $success = false; - $protected = $this->protected_properties(); - if ( $this->{$this->primary_key} ) { - // Update - $success = $wpdb->update( $this->tablename, array_filter( $data, function( $key ) use( $protected ) { - return ! in_array( $key , $protected ); - }, ARRAY_FILTER_USE_KEY ), [$this->primary_key => $this->attributes[$this->primary_key]] ); - if ( $success ) { - foreach ( $data as $key => $value ) { - $this->$key = $value; - } - do_action( 'data_model_' . $this->table . '_updated', $this ); - } - } - return $success; - } - /** - * Deletes where query. - * @since 1.0.0 - * - * @global object Wordpress Data base accessor. - * - * @param array $args Query arguments. - * - * @return bool - */ - protected function _delete_where( $args ) - { - global $wpdb; - return $wpdb->delete( $this->tablename, $args ); - } +abstract class DataModel implements Arrayable, Stringable, JSONable { + + const TABLE = ''; + + /** + * Holds the model data. + * @since 1.0.0 + * + * @var array + */ + protected $attributes = []; + /** + * Holds the list of attributes or properties that should be part of the data casted to array or string. + * Those not listed in this array will remain as hidden. + * @since 1.0.0 + * + * @var array + */ + protected $properties = []; + + /** + * Database table name. + * @since 1.0.0 + * @var string + */ + protected $table = ''; + /** + * Reference to primary key column name. + * @since 1.0.0 + * @var string + */ + protected $primary_key = 'ID'; + /** + * List of properties used for keyword search. + * @since 1.0.0 + * @var array + */ + protected static $keywords = []; + + /** + * Default model constructor. + * + * @param array $attributes + * @param mixed $id + * + * @since 1.0.0 + * + */ + public function __construct( $attributes = [], $id = null ) { + $this->attributes = $attributes; + if ( ! empty( $id ) ) { + $this->attributes[ $this->primary_key ] = $id; + } + } + + /** + * Getter property. + * Returns value as reference, reference to aliases based on functions will not work. + * @since 1.0.0 + */ + public function &__get( $property ) { + $value = null; + // Protected properties + if ( property_exists( $this, $property ) ) { + return $this->$property; + } + // Normal data handled in attributes + if ( isset( $this->attributes[ $property ] ) ) { + return $this->attributes[ $property ]; + } + // Aliases + if ( method_exists( $this, 'get' . ucfirst( $property ) . 'Alias' ) ) { + $value = call_user_func_array( [ &$this, 'get' . ucfirst( $property ) . 'Alias' ], [] ); + } + + return $value; + } + + /** + * Setter property values. + * @since 1.0.0 + */ + public function __set( $property, $value ) { + if ( property_exists( $this, $property ) ) { + // Protected properties + $this->$property = $value; + } elseif ( method_exists( $this, 'set' . ucfirst( $property ) . 'Alias' ) ) { + // Aliases + call_user_func_array( [ &$this, 'set' . ucfirst( $property ) . 'Alias' ], [ $value ] ); + } else { + // Normal attribute + $this->attributes[ $property ] = $value; + } + } + + /** + * Static constructor that finds recond in database + * and fills model. + * + * @param mixed $id + * + * @return DataModel|null + * @throws Exception + * @since 1.0.0 + * + */ + public static function find( $id ) { + $model = new static( [], $id ); + + return $model->load(); + } + + /** + * Static constructor that finds recond in database + * and fills model using where statement. + * + * @param array $args Where query statement arguments. See non-static method. + * + * @return DataModel + * @throws Exception + * @since 1.0.0 + * + */ + public static function find_where( $args ) { + $model = new static; + + return $model->load_where( $args ); + } + + /** + * Static constructor that inserts recond in database and fills model. + * + * @param array $attributes + * + * @return DataModel + * @since 1.0.0 + * + */ + public static function insert( $attributes ) { + $model = new static( $attributes ); + + return $model->save( true ) ? $model : null; + } + + /** + * Static constructor that deletes records + * + * @param array $args Where query statement arguments. See non-static method. + * + * @return bool + * @since 1.0.0 + * + */ + public static function delete_where( $args ) { + $model = new static( [] ); + + return $model->_delete_where( $args ); + } + + /** + * Returns a collection of models. + * @return array + * @throws Exception + * @since 1.0.0 + * + */ + public static function where( $args = [] ) { + // Pull specific data from args + $limit = isset( $args['limit'] ) ? $args['limit'] : null; + unset( $args['limit'] ); + $offset = isset( $args['offset'] ) ? $args['offset'] : 0; + unset( $args['offset'] ); + $keywords = isset( $args['keywords'] ) ? $args['keywords'] : null; + unset( $args['keywords'] ); + $keywords_separator = isset( $args['keywords_separator'] ) ? $args['keywords_separator'] : ' '; + unset( $args['keywords_separator'] ); + $order_by = isset( $args['order_by'] ) ? $args['order_by'] : null; + unset( $args['order_by'] ); + $order = isset( $args['order'] ) ? $args['order'] : 'ASC'; + unset( $args['order'] ); + // Build query and retrieve + $builder = new QueryBuilder( static::TABLE . '_where' ); + + return array_map( + function ( $attributes ) { + return new static( $attributes ); + }, + $builder->select( '*' ) + ->from( static::TABLE . ' as `' . static::TABLE . '`' ) + ->keywords( $keywords, static::$keywords, $keywords_separator ) + ->where( $args ) + ->order_by( $order_by, $order ) + ->limit( $limit ) + ->offset( $offset ) + ->get( ARRAY_A ) + ); + } + + /** + * Returns count. + * @return int + * @throws Exception + * @since 1.0.0 + * + */ + public static function count( $args = [] ) { + // Pull specific data from args + unset( $args['limit'] ); + unset( $args['offset'] ); + $keywords = isset( $args['keywords'] ) ? sanitize_text_field( $args['keywords'] ) : null; + unset( $args['keywords'] ); + // Build query and retrieve + $builder = new QueryBuilder( static::TABLE . '_count' ); + + return $builder->from( static::TABLE . ' as `' . static::TABLE . '`' ) + ->keywords( $keywords, static::$keywords ) + ->where( $args ) + ->count(); + } + + /** + * Returns initialized builder with model set in from statement. + * @return QueryBuilder + * @since 1.0.0 + * + */ + public static function builder() { + $builder = new QueryBuilder( static::TABLE . '_custom' ); + + return $builder->from( static::TABLE . ' as `' . static::TABLE . '`' ); + } + + /** + * Returns a collection with all models found in the database. + * @return array + * @since 1.0.7 + * + */ + public static function all() { + // Build query and retrieve + $builder = new QueryBuilder( static::TABLE . '_all' ); + + return array_map( + function ( $attributes ) { + return new static( $attributes ); + }, + $builder->select( '*' ) + ->from( static::TABLE . ' as `' . static::TABLE . '`' ) + ->get( ARRAY_A ) + ); + } + + /** + * Returns query results from mass update. + * + * @param array $set Set of column => data to update. + * @param array $where Where condition. + * + * @return bool + * @throws Exception + * @since 1.0.12 + */ + public static function update_all( $set, $where = [] ) { + $builder = new QueryBuilder( static::TABLE . '_static_update' ); + + return $builder->from( static::TABLE ) + ->set( $set ) + ->where( $where ) + ->update(); + } + + /** + * Returns `tablename` property. + * @return string + * @since 1.0.0 + * + */ + protected function getTablenameAlias() { + global $wpdb; + + return $wpdb->prefix . $this->table; + } + + /** + * Returns list of protected/readonly properties for + * when saving or updating. + * @return array + * @since 1.0.0 + * + */ + protected function protected_properties() { + return apply_filters( + 'data_model_' . $this->table . '_excluded_save_fields', + [ $this->primary_key, 'created_at', 'updated_at' ], + $this->table_name() + ); + } + + /** + * Saves data attributes in database. + * Returns flag indicating if save process was successful. + * + * @param bool $force_insert Flag that indicates if should insert regardless of ID. + * + * @return bool + * @since 1.0.0 + * + */ + public function save( $force_insert = false ) { + global $wpdb; + $protected = $this->protected_properties(); + if ( ! $force_insert && $this->{$this->primary_key} ) { + // Update + $success = $wpdb->update( $this->table_name(), array_filter( $this->attributes, function ( $key ) use ( $protected ) { + return ! in_array( $key, $protected ); + }, ARRAY_FILTER_USE_KEY ), [ $this->primary_key => $this->attributes[ $this->primary_key ] ] ); + if ( $success ) { + do_action( 'data_model_' . $this->table . '_updated', $this ); + } + } else { + // Insert + $success = $wpdb->insert( $this->table_name(), array_filter( $this->attributes, function ( $key ) use ( $protected ) { + return ! in_array( $key, $protected ); + }, ARRAY_FILTER_USE_KEY ) ); + $this->{$this->primary_key} = $wpdb->insert_id; + $date = date( 'Y-m-d H:i:s' ); + $this->created_at = $date; + $this->updated_at = $date; + if ( $success ) { + do_action( 'data_model_' . $this->table . '_inserted', $this ); + } + } + if ( $success ) { + do_action( 'data_model_' . $this->table . '_save', $this ); + } + + return $success; + } + + /** + * Loads attributes from database. + * @return DataModel|null + * @throws Exception + * @since 1.0.0 + * + */ + public function load() { + $builder = new QueryBuilder( $this->table . '_load' ); + $this->attributes = $builder->select( '*' ) + ->from( $this->table ) + ->where( [ $this->primary_key => $this->attributes[ $this->primary_key ] ] ) + ->first( ARRAY_A ); + + return ! empty( $this->attributes ) + ? apply_filters( 'data_model_' . $this->table, $this ) + : null; + } + + /** + * Loads attributes from database based on custome where statements + * + * Samples: + * // Simple query + * $this->load_where( ['slug' => 'this-example-1'] ); + * // Compound query with OR statement + * $this->load_where( ['ID' => 77, 'ID' => ['OR', 546]] ); + * + * @param array $args Query arguments. + * + * @return DataModel|null + * @throws Exception + * + * @since 1.0.0 + * + */ + public function load_where( $args ) { + if ( empty( $args ) ) { + return null; + } + if ( ! is_array( $args ) ) { + throw new Exception( 'Arguments parameter must be an array.', 10100 ); + } + $builder = new QueryBuilder( $this->table . '_load_where' ); + $this->attributes = $builder->select( '*' ) + ->from( $this->table ) + ->where( $args ) + ->first( ARRAY_A ); + + return ! empty( $this->attributes ) + ? apply_filters( 'data_model_' . $this->table, $this ) + : null; + } + + /** + * Deletes record. + * @return bool + * @since 1.0.0 + * + */ + public function delete() { + global $wpdb; + $deleted = $this->{$this->primary_key} + ? $wpdb->delete( $this->table_name(), [ $this->primary_key => $this->attributes[ $this->primary_key ] ] ) + : false; + if ( $deleted ) { + do_action( 'data_model_' . $this->table . '_deleted', $this ); + } + + return $deleted; + } + + /** + * Updates specific columns of the model (not the whole object like save()). + * + * @param array $data Data to update. + * + * @return bool + * @since 1.0.12 + * + */ + public function update( $data = [] ) { + // If not data, let save() handle this + if ( empty( $data ) || ! is_array( $data ) ) { + return $this->save(); + } + global $wpdb; + $success = false; + $protected = $this->protected_properties(); + if ( $this->{$this->primary_key} ) { + // Update + $success = $wpdb->update( $this->table_name(), array_filter( $data, function ( $key ) use ( $protected ) { + return ! in_array( $key, $protected ); + }, ARRAY_FILTER_USE_KEY ), [ $this->primary_key => $this->attributes[ $this->primary_key ] ] ); + if ( $success ) { + foreach ( $data as $key => $value ) { + $this->$key = $value; + } + do_action( 'data_model_' . $this->table . '_updated', $this ); + } + } + + return $success; + } + + /** + * Deletes where query. + * + * @param array $args Query arguments. + * + * @return bool + * @since 1.0.0 + * + */ + protected function _delete_where( $args ) { + global $wpdb; + + return $wpdb->delete( $this->table_name(), $args ); + } + + /** + * Prints the full table name + * + * @return string + * @since 1.1.0 + * + */ + public function table_name() { + global $wpdb; + return $wpdb->prefix . $this->table; + } + + /** + * Returns model as array. + * @since 1.1.0 + * + * @return array + */ + public function __toArray() + { + $output = []; + foreach ($this->properties as $property) { + if ($this->$property !== null) + $output[$property] = $this->__getCleaned($this->$property); + } + return $output; + } + /** + * Returns model as array. + * @since 1.1.0 + * + * @return array + */ + public function toArray() + { + return $this->__toArray(); + } + /** + * Returns model as json string. + * @since 1.1.0 + * + * @return string + */ + public function __toString() + { + return json_encode($this->__toArray()); + } + /** + * Returns cleaned value for casting. + * @since 1.1.0 + * + * @param mixed $value Value to clean. + * + * @return mixed + */ + private function __getCleaned($value) + { + switch (gettype($value)) { + case 'object': + return method_exists($value, '__toArray') + ? $value->__toArray() + : (method_exists($value, 'toArray') + ? $value->toArray() + :(array)$value + ); + case 'array': + $output = []; + foreach ($value as $key => $data) { + if ($data !== null) + $output[$key] = $this->__getCleaned($data); + } + return $output; + } + return $value; + } + /** + * Returns object as JSON string. + * @since 1.1.0 + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $options JSON encoding options. See @link. + * @param int $depth JSON encoding depth. See @link. + * + * @return string + */ + public function __toJSON($options = 0, $depth = 512) + { + return json_encode($this->__toArray(), $options, $depth); + } + /** + * Returns object as JSON string. + * @since 1.1.0 + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $options JSON encoding options. See @link. + * @param int $depth JSON encoding depth. See @link. + * + * @return string + */ + public function toJSON($options = 0, $depth = 512) + { + return $this->__toJSON($options, $depth); + } } \ No newline at end of file diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 0000000..fee69f0 --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,144 @@ + + * @copyright Darko G + * @license MIT + * @package IgniteKit\WP\QueryBuilder + * @version 1.0.2 + */ +class Collection extends ArrayObject implements Arrayable, JSONable, Stringable +{ + /** + * Returns collection sorted by specific attribute name. + * Only works with Models or arrays with string defined keys. + * @since 1.0.2 + * + * @link http://php.net/manual/en/array.constants.php + * + * @param string $attribute Attribute to sort by. + * @param string $sortFlag Sort direction. Use PHP sort constants + * + * @return array (this for chaining) + */ + public function sortBy($attribute, $sortFlag = SORT_REGULAR) + { + $values = array(); + for ($i = count( $this ) -1; $i >= 0; --$i) { + $values[] = $this[$i]->$attribute; + } + $values = array_unique($values); + sort($values, $sortFlag); + $new = new self(); + foreach ($values as $value) { + for ($i = count($this) -1; $i >= 0; --$i) { + if ($value == $this[$i]->$attribute) { + $new[] = $this[$i]; + } + } + } + return $new; + } + + /** + * Returns collection grouped by an attribute's name. + * Only works with Models or arrays with string defined keys. + * @since 1.0.2 + * + * @param string $attribute Attribute to group by. + * + * @return array (this for chaining) + */ + public function groupby($attribute) + { + $new = new self(); + for ($i = 0; $i < count( $this ); ++$i) { + $key = (string)$this[$i]->$attribute; + if (!isset( $new[$key])) + $new[$key] = new self(); + $new[$key][] = $this[$i]; + } + return $new; + } + /** + * Returns collection as pure array. + * Does depth array casting. + * @since 1.0.2 + * + * @return array + */ + public function __toArray() + { + $output = []; + $value = null; + foreach ($this as $key => $value) { + $output[$key] = !is_object($value) + ? $value + : (method_exists($value, '__toArray') + ? $value->__toArray() + : (array)$value + ); + } + return $output; + } + /** + * Returns collection as pure array. + * Does depth array casting. + * @since 1.0.2 + * + * @return array + */ + public function toArray() + { + return $this->__toArray(); + } + /** + * Returns collection as a string. + * @since 1.0.2 + * + * @param string + */ + public function __toString() + { + return json_encode($this->__toArray()); + } + /** + * Returns object as JSON string. + * @since 1.0.2 + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $options JSON encoding options. See @link. + * @param int $depth JSON encoding depth. See @link. + * + * @return string + */ + public function __toJSON($options = 0, $depth = 512) + { + return json_encode($this->__toArray(), $options, $depth); + } + /** + * Returns object as JSON string. + * @since 1.0.2 + * + * @link http://php.net/manual/en/function.json-encode.php + * + * @param int $options JSON encoding options. See @link. + * @param int $depth JSON encoding depth. See @link. + * + * @return string + */ + public function toJSON($options = 0, $depth = 512) + { + return $this->__toJSON($options, $depth); + } +} \ No newline at end of file diff --git a/src/Contracts/Arrayable.php b/src/Contracts/Arrayable.php new file mode 100644 index 0000000..a6692e1 --- /dev/null +++ b/src/Contracts/Arrayable.php @@ -0,0 +1,25 @@ + + * @copyright Darko G + * @license MIT + * @version 1.0.0 + */ +interface Arrayable +{ + /** + * Returns object as string. + * @since 1.0.0 + */ + public function __toArray(); + /** + * Returns object as string. + * @since 1.0.0 + */ + public function toArray(); +} \ No newline at end of file diff --git a/src/Contracts/JSONable.php b/src/Contracts/JSONable.php new file mode 100644 index 0000000..93a4354 --- /dev/null +++ b/src/Contracts/JSONable.php @@ -0,0 +1,26 @@ + + * @copyright Darko G + * @license MIT + * @package IgniteKit\WP\QueryBuilder\Contracts + * @version 1.0.2 + */ +interface JSONable +{ + /** + * Returns object as JSON string. + * @since 1.0.2 + */ + public function __toJSON($options = 0, $depth = 512); + /** + * Returns object as JSON string. + * @since 1.0.2 + */ + public function toJSON($options = 0, $depth = 512); +} \ No newline at end of file diff --git a/src/Contracts/Stringable.php b/src/Contracts/Stringable.php new file mode 100644 index 0000000..8005e6f --- /dev/null +++ b/src/Contracts/Stringable.php @@ -0,0 +1,21 @@ + + * @copyright Darko G + * @license MIT + * @package IgniteKit\WP\QueryBuilder\Contracts + * @version 1.0.0 + */ +interface Stringable +{ + /** + * Returns object as string. + * @since 1.0.0 + */ + public function __toString(); +} \ No newline at end of file diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index e134ab2..b2ec554 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -1,932 +1,1124 @@ Hyper Tribal - * @author 10 Quality + * + * @copyright 10 Quality + * @copyright Darko G * @license MIT * @package wp-query-builder * @version 1.0.13 */ -class QueryBuilder -{ - /** - * Builder ID for hook references. - * @since 1.0.0 - * @var string - */ - protected $id; - /** - * Builder statements. - * @since 1.0.0 - * @var array - */ - protected $builder; - /** - * Builder options. - * @since 1.0.11 - * @var array - */ - protected $options; - /** - * Builder constructor. - * @since 1.0.0 - * - * @param string|null $id - */ - public function __construct( $id = null ) - { - $this->id = ! empty( $id ) ? $id : uniqid(); - $this->builder = [ - 'select' => [], - 'from' => null, - 'join' => [], - 'where' => [], - 'order' => [], - 'group' => [], - 'having' => null, - 'limit' => null, - 'offset' => 0, - 'set' => [], - ]; - $this->options = [ - 'wildcard' => '{%}', - 'default_wildcard' => '{%}', - ]; - } - /** - * Static constructor. - * @since 1.0.0 - * - * @param string $id - */ - public static function create( $id = null ) - { - $builder = new self( $id ); - return $builder; - } - /** - * Adds select statement. - * @since 1.0.0 - * - * @param array|string $statement - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function select( $statement ) - { - $this->builder['select'][] = $statement; - return $this; - } - /** - * Adds from statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param string $from - * @param bool $add_prefix Should DB prefix be added. - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function from( $from, $add_prefix = true ) - { - global $wpdb; - $this->builder['from'] = ( $add_prefix ? $wpdb->prefix : '' ) . $from; - return $this; - } - /** - * Adds keywords search statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param string $keywords Searched keywords. - * @param array $columns Column or fields where to search. - * @param string $separator Keyword separator within keywords string. - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function keywords( $keywords, $columns, $separator = ' ' ) - { - if ( ! empty( $keywords ) ) { - global $wpdb; - foreach ( explode( $separator , $keywords ) as $keyword ) { - $keyword = '%' . $this->sanitize_value( true, $keyword ) . '%'; - $this->builder['where'][] = [ - 'joint' => 'AND', - 'condition' => '(' . implode( ' OR ', array_map( function( $column ) use( &$wpdb, &$keyword ) { - return $wpdb->prepare( $column . ' LIKE %s', $keyword ); - }, $columns ) ) . ')', - ]; - } - } - return $this; - } - /** - * Adds where statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param array $args Multiple where arguments. - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function where( $args ) - { - global $wpdb; - foreach ( $args as $key => $value ) { - // Options - set - if ( is_array( $value ) && array_key_exists( 'wildcard', $value ) && !empty( $value['wildcard'] ) ) - $this->options['wildcard'] = trim( $value['wildcard'] ); - // Value - $arg_value = is_array( $value ) && array_key_exists( 'value', $value ) ? $value['value'] : $value; - if ( is_array( $value ) && array_key_exists( 'min', $value ) ) - $arg_value = $value['min']; - $sanitize_callback = is_array( $value ) && array_key_exists( 'sanitize_callback', $value ) - ? $value['sanitize_callback'] - : true; - if ( $sanitize_callback - && $key !== 'raw' - && ( !is_array( $value ) || !array_key_exists( 'key', $value ) ) - ) - $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); - $statement = $key === 'raw' - ? [$arg_value] - : [ - $key, - is_array( $value ) && isset( $value['operator'] ) ? strtoupper( $value['operator'] ) : ( $arg_value === null ? 'is' : '=' ), - is_array( $value ) && array_key_exists( 'key', $value ) - ? $value['key'] - : ( is_array( $arg_value ) - ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) - : ( $arg_value === null - ? 'null' - : $wpdb->prepare( ( !is_array( $value ) || !array_key_exists( 'force_string', $value ) || !$value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s' , $arg_value ) - ) - ), - ]; - // Between? - if ( is_array( $value ) && isset( $value['operator'] ) ) { - $value['operator'] = strtoupper( $value['operator'] ); - if ( strpos( $value['operator'], 'BETWEEN' ) !== false ) { - if ( array_key_exists( 'max', $value ) || array_key_exists( 'key_b', $value ) ) { - if ( array_key_exists( 'max', $value ) ) - $arg_value = $value['max']; - if ( array_key_exists( 'sanitize_callback2', $value ) ) - $sanitize_callback = $value['sanitize_callback2']; - if ( $sanitize_callback && !array_key_exists( 'key_b', $value ) ) - $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); - $statement[] = 'AND'; - $statement[] = array_key_exists( 'key_b', $value ) - ? $value['key_b'] - : ( is_array( $arg_value ) - ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) - : $wpdb->prepare( ( !array_key_exists( 'force_string', $value ) || !$value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s' , $arg_value ) - ); - } else { - throw new Exception( '"max" or "key_b "parameter must be indicated when using the BETWEEN operator.', 10202 ); - } - } - } - $this->builder['where'][] = [ - 'joint' => is_array( $value ) && isset( $value['joint'] ) ? $value['joint'] : 'AND', - 'condition' => implode( ' ', $statement ), - ]; - // Options - reset - if ( is_array( $value ) && array_key_exists( 'wildcard', $value ) && !empty( $value['wildcard'] ) ) - $this->options['wildcard'] = $this->options['default_wildcard']; - } - return $this; - } - /** - * Adds join statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @throws Exception - * - * @param string $table Join table. - * @param array $args Join arguments. - * @param bool|string $type Flag that indicates if it is "LEFT or INNER", also accepts direct join string. - * @param bool $add_prefix Should DB prefix be added. - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function join( $table, $args, $type = false, $add_prefix = true ) - { - $type = is_string( $type ) ? strtoupper( trim( $type ) ) : ( $type ? 'LEFT' : '' ); - if ( !in_array( $type, ['', 'LEFT', 'RIGHT', 'INNER', 'CROSS', 'LEFT OUTER', 'RIGHT OUTER'] ) ) - throw new Exception( 'Invalid join type.', 10201 ); - global $wpdb; - $join = [ - 'table' => ( $add_prefix ? $wpdb->prefix : '' ) . $table, - 'type' => $type, - 'on' => [], - ]; - foreach ( $args as $argument ) { - // Options - set - if ( array_key_exists( 'wildcard', $argument ) && !empty( $argument['wildcard'] ) ) - $this->options['wildcard'] = trim( $argument['wildcard'] ); - // Value - $arg_value = isset( $argument['value'] ) ? $argument['value'] : null; - if ( array_key_exists( 'min', $argument ) ) - $arg_value = $argument['min']; - $sanitize_callback = array_key_exists( 'sanitize_callback', $argument ) ? $argument['sanitize_callback'] : true; - if ( $sanitize_callback - && !array_key_exists( 'raw', $argument ) - && !array_key_exists( 'key_b', $argument ) - ) - $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); - $statement = array_key_exists( 'raw', $argument ) - ? [$argument['raw']] - : [ - isset( $argument['key_a'] ) ? $argument['key_a'] : $argument['key'], - isset( $argument['operator'] ) ? strtoupper( $argument['operator'] ) : ( $arg_value === null && ! isset( $argument['key_b'] ) ? 'is' : '=' ), - array_key_exists( 'key_b', $argument ) - ? $argument['key_b'] - : ( is_array( $arg_value ) - ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) - : ( $arg_value === null - ? 'null' - : $wpdb->prepare( ( !array_key_exists( 'force_string', $argument ) || !$argument['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s' , $arg_value ) - ) - ), - ]; - // Between? - if ( isset( $argument['operator'] ) ) { - $argument['operator'] = strtoupper( $argument['operator'] ); - if ( strpos( $argument['operator'], 'BETWEEN' ) !== false ) { - if ( array_key_exists( 'max', $argument ) || array_key_exists( 'key_c', $argument ) ) { - if ( array_key_exists( 'max', $argument ) ) - $arg_value = $argument['max']; - if ( array_key_exists( 'sanitize_callback2', $argument ) ) - $sanitize_callback = $argument['sanitize_callback2']; - if ( $sanitize_callback && !array_key_exists( 'key_c', $argument ) ) - $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); - $statement[] = 'AND'; - $statement[] = array_key_exists( 'key_c', $argument ) - ? $argument['key_c'] - : ( is_array( $arg_value ) - ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) - : $wpdb->prepare( ( !array_key_exists( 'force_string', $argument ) || !$argument['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s' , $arg_value ) - ); - } else { - throw new Exception( '"max" or "key_c" parameter must be indicated when using the BETWEEN operator.', 10203 ); - } - } - } - $join['on'][] = [ - 'joint' => isset( $argument['joint'] ) ? $argument['joint'] : 'AND', - 'condition' => implode( ' ', $statement ), - ]; - // Options - reset - if ( array_key_exists( 'wildcard', $argument ) && !empty( $argument['wildcard'] ) ) - $this->options['wildcard'] = $this->options['default_wildcard']; - } - $this->builder['join'][] = $join; - return $this; - } - /** - * Adds limit statement. - * @since 1.0.0 - * - * @param int $limit - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function limit( $limit ) - { - $this->builder['limit'] = $limit; - return $this; - } - /** - * Adds offset statement. - * @since 1.0.0 - * - * @param int $offset - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function offset( $offset ) - { - $this->builder['offset'] = $offset; - return $this; - } - /** - * Adds order by statement. - * @since 1.0.0 - * - * @param string $key - * @param string $direction - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function order_by( $key, $direction = 'ASC' ) - { - $direction = trim( strtoupper( $direction ) ); - if ( $direction !== 'ASC' && $direction !== 'DESC' ) - throw new Exception( 'Invalid direction value.', 10200 ); - if ( ! empty( $key ) ) - $this->builder['order'][] = $key . ' ' . $direction; - return $this; - } - /** - * Adds group by statement. - * @since 1.0.0 - * - * @param string $statement - * - * @return \TenQuality\WP\Database\Utility\QueryBuilder this for chaining. - */ - public function group_by( $statement ) - { - if ( ! empty( $statement ) ) - $this->builder['group'][] = $statement; - return $this; - } - /** - * Adds having statement. - * @since 1.0.0 - * - * @param string $statement - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function having( $statement ) - { - if ( ! empty( $statement ) ) - $this->builder['having'] = $statement; - return $this; - } - /** - * Adds set statement (for update). - * @since 1.0.12 - * - * @global object $wpdb - * - * @param array $args Multiple where arguments. - * - * @return \TenQuality\WP\Database\QueryBuilder this for chaining. - */ - public function set( $args ) - { - global $wpdb; - foreach ( $args as $key => $value ) { - // Value - $arg_value = is_array( $value ) && array_key_exists( 'value', $value ) ? $value['value'] : $value; - $sanitize_callback = is_array( $value ) && array_key_exists( 'sanitize_callback', $value ) - ? $value['sanitize_callback'] - : true; - if ( $sanitize_callback - && $key !== 'raw' - && ( !is_array( $value ) || !array_key_exists( 'raw', $value ) ) - ) - $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); - $statement = $key === 'raw' - ? [$arg_value] - : [ - $key, - '=', - is_array( $value ) && array_key_exists( 'raw', $value ) - ? $value['raw'] - : ( is_array( $arg_value ) - ? ( '\'' . implode( ',', $arg_value ) . '\'' ) - : ( $arg_value === null - ? 'null' - : $wpdb->prepare( ( !is_array( $value ) || !array_key_exists( 'force_string', $value ) || !$value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s' , $arg_value ) - ) - ), - ]; - $this->builder['set'][] = implode( ' ', $statement ); - } - return $this; - } - /** - * Retunrs results from builder statements. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param int $output WPDB output type. - * @param callable $callable_mapping Function callable to filter or map results to. - * @param bool $calc_rows Flag that indicates to SQL if rows should be calculated or not. - * - * @return array - */ - public function get( $output = OBJECT, $callable_mapping = null, $calc_rows = false ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_get_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_get_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_select( $query, $calc_rows ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - $this->_query_order( $query ); - $this->_query_limit( $query ); - $this->_query_offset( $query ); - // Process - $query = apply_filters( 'query_builder_get_query', $query ); - $query = apply_filters( 'query_builder_get_query_' . $this->id, $query ); - $results = $wpdb->get_results( $query, $output ); - if ( $callable_mapping ) { - $results = array_map( function( $row ) use( &$callable_mapping ) { - return call_user_func_array( $callable_mapping, [$row] ); - }, $results ); - } - return $results; - } - /** - * Returns first row found. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param int $output WPDB output type. - * - * @return object|array - */ - public function first( $output = OBJECT ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_first_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_first_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_select( $query ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - $this->_query_order( $query ); - $query .= ' LIMIT 1'; - $this->_query_offset( $query ); - // Process - $query = apply_filters( 'query_builder_first_query', $query ); - $query = apply_filters( 'query_builder_first_query_' . $this->id, $query ); - return $wpdb->get_row( $query, $output ); - } - /** - * Returns a value. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param int $x Column of value to return. Indexed from 0. - * @param int $y Row of value to return. Indexed from 0. - * - * @return mixed - */ - public function value( $x = 0, $y = 0 ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_value_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_value_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_select( $query ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - $this->_query_order( $query ); - $this->_query_limit( $query ); - $this->_query_offset( $query ); - // Process - $query = apply_filters( 'query_builder_value_query', $query ); - $query = apply_filters( 'query_builder_value_query_' . $this->id, $query ); - return $wpdb->get_var( $query, $x, $y ); - } - /** - * Returns the count. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param string|int $column Count column. - * @param bool $bypass_limit Flag that indicates if limit + offset should be considered on count. - * - * @return int - */ - public function count( $column = 1, $bypass_limit = true ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_count_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_count_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = 'SELECT count(' . $column . ') as `count`'; - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - if ( ! $bypass_limit ) { - $this->_query_limit( $query ); - $this->_query_offset( $query ); - } - // Process - $query = apply_filters( 'query_builder_count_query', $query ); - $query = apply_filters( 'query_builder_count_query_' . $this->id, $query ); - return intval( $wpdb->get_var( $query ) ); - } - /** - * Returns column results from builder statements. - * @since 1.0.6 - * - * @global object $wpdb - * - * @param int $x Column index number. - * @param bool $calc_rows Flag that indicates to SQL if rows should be calculated or not. - * - * @return array - */ - public function col( $x = 0, $calc_rows = false ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_col_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_col_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_select( $query, $calc_rows ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - $this->_query_order( $query ); - $this->_query_limit( $query ); - $this->_query_offset( $query ); - // Process - $query = apply_filters( 'query_builder_col_query', $query ); - $query = apply_filters( 'query_builder_col_query_' . $this->id, $query ); - return $wpdb->get_col( $query, $x ); - } - /** - * Returns flag indicating if query has been executed. - * @since 1.0.8 - * - * @global object $wpdb - * - * @param string $sql - * - * @return bool - */ - public function query( $sql = '' ) - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_query_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_query_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = $sql; - if ( empty( $query ) ) { - $this->_query_select( $query, false ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - $this->_query_group( $query ); - $this->_query_having( $query ); - $this->_query_order( $query ); - $this->_query_limit( $query ); - $this->_query_offset( $query ); - } - // Process - $query = apply_filters( 'query_builder_query_query', $query ); - $query = apply_filters( 'query_builder_query_query_' . $this->id, $query ); - return $wpdb->query( $query ); - } - /** - * Returns flag indicating if query has been executed. - * @since 1.0.8 - * - * @see self::query() - * - * @param string $sql - * - * @return bool - */ - public function raw( $sql ) - { - return $this->query( $sql ); - } - /** - * Returns flag indicating if delete query has been executed. - * @since 1.0.8 - * - * @global object $wpdb - * - * @return bool - */ - public function delete() - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_delete_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_delete_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_delete( $query ); - $this->_query_from( $query ); - $this->_query_join( $query ); - $this->_query_where( $query ); - // Process - $query = apply_filters( 'query_builder_delete_query', $query ); - $query = apply_filters( 'query_builder_delete_query_' . $this->id, $query ); - return $wpdb->query( $query ); - } - /** - * Returns flag indicating if update query has been executed. - * @since 1.0.12 - * - * @global object $wpdb - * - * @return bool - */ - public function update() - { - global $wpdb; - $this->builder = apply_filters( 'query_builder_update_builder', $this->builder ); - $this->builder = apply_filters( 'query_builder_update_builder_' . $this->id, $this->builder ); - // Build - // Query - $query = ''; - $this->_query_update( $query ); - $this->_query_join( $query ); - $this->_query_set( $query ); - $this->_query_where( $query ); - // Process - $query = apply_filters( 'query_builder_update_query', $query ); - $query = apply_filters( 'query_builder_update_query_' . $this->id, $query ); - return $wpdb->query( $query ); - } - /** - * Retunrs found rows in last query, if SQL_CALC_FOUND_ROWS is used and is supported. - * @since 1.0.6 - * - * @global object $wpdb - * - * @return array - */ - public function rows_found() - { - global $wpdb; - $query = 'SELECT FOUND_ROWS()'; - // Process - $query = apply_filters( 'query_builder_found_rows_query', $query ); - $query = apply_filters( 'query_builder_found_rows_query_' . $this->id, $query ); - return $wpdb->get_var( $query ); - } - /** - * Builds query's select statement. - * @since 1.0.0 - * - * @param string &$query - * @param bool $calc_rows - */ - private function _query_select( &$query, $calc_rows = false ) - { - $query = 'SELECT ' . ( $calc_rows ? 'SQL_CALC_FOUND_ROWS ' : '' ) . ( - is_array( $this->builder['select'] ) && count( $this->builder['select'] ) - ? implode( ',' , $this->builder['select'] ) - : '*' - ); - } - /** - * Builds query's from statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_from( &$query ) - { - $query .= ' FROM ' . $this->builder['from']; - } - /** - * Builds query's join statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_join( &$query ) - { - foreach ( $this->builder['join'] as $join ) { - $query .= ( !empty( $join['type'] ) ? ' ' . $join['type'] . ' JOIN ' : ' JOIN ' ) . $join['table']; - for ( $i = 0; $i < count( $join['on'] ); ++$i ) { - $query .= ( $i === 0 ? ' ON ' : ' ' . $join['on'][$i]['joint'] . ' ' ) - . $join['on'][$i]['condition']; - } - } - } - /** - * Builds query's where statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_where( &$query ) - { - for ( $i = 0; $i < count( $this->builder['where'] ); ++$i ) { - $query .= ( $i === 0 ? ' WHERE ' : ' ' . $this->builder['where'][$i]['joint'] . ' ' ) - . $this->builder['where'][$i]['condition']; - } - } - /** - * Builds query's group by statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_group( &$query ) - { - if ( count( $this->builder['group'] ) ) - $query .= ' GROUP BY ' . implode( ',', $this->builder['group'] ); - } - /** - * Builds query's having statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_having( &$query ) - { - if ( $this->builder['having'] ) - $query .= ' HAVING ' . $this->builder['having']; - } - /** - * Builds query's order by statement. - * @since 1.0.0 - * - * @param string &$query - */ - private function _query_order( &$query ) - { - if ( count( $this->builder['order'] ) ) - $query .= ' ORDER BY ' . implode( ',', $this->builder['order'] ); - } - /** - * Builds query's limit statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param string &$query - */ - private function _query_limit( &$query ) - { - global $wpdb; - if ( $this->builder['limit'] ) - $query .= $wpdb->prepare( ' LIMIT %d', $this->builder['limit'] ); - } - /** - * Builds query's offset statement. - * @since 1.0.0 - * - * @global object $wpdb - * - * @param string &$query - */ - private function _query_offset( &$query ) - { - global $wpdb; - if ( $this->builder['offset'] ) - $query .= $wpdb->prepare( ' OFFSET %d', $this->builder['offset'] ); - } - /** - * Builds query's delete statement. - * @since 1.0.8 - * - * @param string &$query - */ - private function _query_delete( &$query ) - { - $query .= trim( 'DELETE ' . ( count( $this->builder['join'] ) - ? preg_replace( '/\s[aA][sS][\s\S]+.*?/', '', $this->builder['from'] ) - : '' - ) ); - } - /** - * Builds query's update statement. - * @since 1.0.12 - * - * @param string &$query - */ - private function _query_update( &$query ) - { - $query .= trim( 'UPDATE ' . ( count( $this->builder['join'] ) - ? $this->builder['from'] . ',' . implode( ',', array_map( function( $join ) { - return $join['table']; - }, $this->builder['join'] ) ) - : $this->builder['from'] - ) ); - } - /** - * Builds query's set statement. - * @since 1.0.12 - * - * @param string &$query - */ - private function _query_set( &$query ) - { - $query .= $this->builder['set'] ? ' SET ' . implode( ',', $this->builder['set'] ) : ''; - } - /** - * Sanitize value. - * @since 1.0.0 - * - * @param string|bool $callback Sanitize callback. - * @param mixed $value - * - * @return mixed - */ - private function sanitize_value( $callback, $value ) - { - if ( $callback === true ) - $callback = ( is_numeric( $value ) && strpos( $value, '.' ) !== false ) - ? 'floatval' - : ( is_numeric( $value ) - ? 'intval' - : ( is_string( $value ) - ? 'sanitize_text_field' - : null - ) - ); - if ( $callback && strpos( $callback, '_builder' ) !== false ) - $callback = [&$this, $callback]; - if ( is_array( $value ) ) - for ( $i = count( $value ) -1; $i >= 0; --$i ) { - $value[$i] = $this->sanitize_value( true, $value[$i] ); - } - return $callback && is_callable( $callback ) ? call_user_func_array( $callback, [$value] ) : $value; - } - /** - * Returns value escaped with WPDB `esc_like`, - * @since 1.0.6 - * - * @param mixed $value - * - * @return string - */ - private function _builder_esc_like( $value ) - { - global $wpdb; - $wildcard = $this->options['wildcard']; - return implode( '%', array_map( function( $part ) use( &$wpdb, &$wildcard ) { - return $wpdb->esc_like( $part ); - }, explode( $wildcard, $value ) ) ) ; - } - /** - * Returns escaped value for LIKE comparison and appends wild card at the beggining. - * @since 1.0.6 - * - * @param mixed $value - * - * @return string - */ - private function _builder_esc_like_wild_value( $value ) - { - return '%' . $this->_builder_esc_like( $value ); - } - /** - * Returns escaped value for LIKE comparison and appends wild card at the end. - * @since 1.0.6 - * - * @param mixed $value - * - * @return string - */ - private function _builder_esc_like_value_wild( $value ) - { - return $this->_builder_esc_like( $value ) . '%'; - } - /** - * Returns escaped value for LIKE comparison and appends wild cards at both ends. - * @since 1.0.6 - * - * @param mixed $value - * - * @return string - */ - private function _builder_esc_like_wild_wild( $value ) - { - return '%' . $this->_builder_esc_like( $value ) . '%'; - } +class QueryBuilder { + + /** + * Builder ID for hook references. + * @since 1.0.0 + * @var string + */ + protected $id; + /** + * Builder statements. + * @since 1.0.0 + * @var array + */ + protected $builder; + /** + * Builder options. + * @since 1.0.11 + * @var array + */ + protected $options; + + /** + * Builder constructor. + * + * @param string|null $id + * + * @since 1.0.0 + * + */ + public function __construct( $id = null ) { + $this->id = ! empty( $id ) ? $id : uniqid(); + $this->builder = [ + 'select' => [], + 'from' => null, + 'join' => [], + 'where' => [], + 'order' => [], + 'group' => [], + 'having' => null, + 'limit' => null, + 'offset' => 0, + 'set' => [], + ]; + $this->options = [ + 'wildcard' => '{%}', + 'default_wildcard' => '{%}', + ]; + } + + /** + * Static constructor. + * + * @param string $id + * + * @since 1.0.0 + * + */ + public static function create( $id = null ) { + return new self( $id ); + } + + /** + * Adds select statement. + * + * @param array|string $statement + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + * + */ + public function select( $statement ) { + $this->builder['select'][] = $statement; + + return $this; + } + + /** + * Adds from statement. + * + * @param string $from + * @param bool $add_prefix Should DB prefix be added. + * + * @return QueryBuilder this for chaining. + * + * @since 1.0.0 + * + */ + public function from( $from, $add_prefix = true ) { + global $wpdb; + $this->builder['from'] = ( $add_prefix ? $wpdb->prefix : '' ) . $from; + + return $this; + } + + /** + * Adds keywords search statement. + * + * @param string $keywords Searched keywords. + * @param array $columns Column or fields where to search. + * @param string $separator Keyword separator within keywords string. + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + ** + */ + public function keywords( $keywords, $columns, $separator = ' ' ) { + if ( ! empty( $keywords ) ) { + global $wpdb; + foreach ( explode( $separator, $keywords ) as $keyword ) { + $keyword = '%' . $this->sanitize_value( true, $keyword ) . '%'; + $this->builder['where'][] = [ + 'joint' => 'AND', + 'condition' => '(' . implode( ' OR ', array_map( function ( $column ) use ( &$wpdb, &$keyword ) { + return $wpdb->prepare( $column . ' LIKE %s', $keyword ); + }, $columns ) ) . ')', + ]; + } + } + + return $this; + } + + /** + * Adds where statement. + * + * @param array $args Multiple where arguments. + * + * @return QueryBuilder this for chaining. + * @throws Exception + * @since 1.0.0 + * + */ + public function where( $args ) { + + global $wpdb; + foreach ( $args as $key => $value ) { + // Options - set + if ( is_array( $value ) && array_key_exists( 'wildcard', $value ) && ! empty( $value['wildcard'] ) ) { + $this->options['wildcard'] = trim( $value['wildcard'] ); + } + // Value + $arg_value = is_array( $value ) && array_key_exists( 'value', $value ) ? $value['value'] : $value; + if ( is_array( $value ) && array_key_exists( 'min', $value ) ) { + $arg_value = $value['min']; + } + $sanitize_callback = is_array( $value ) && array_key_exists( 'sanitize_callback', $value ) + ? $value['sanitize_callback'] + : true; + if ( $sanitize_callback + && $key !== 'raw' + && ( ! is_array( $value ) || ! array_key_exists( 'key', $value ) ) + ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + + + $statement = $key === 'raw' + ? [ $arg_value ] + : [ + $key, + is_array( $value ) && isset( $value['operator'] ) ? strtoupper( $value['operator'] ) : ( $arg_value === null ? 'is' : '=' ), + is_array( $value ) && array_key_exists( 'key', $value ) + ? $value['key'] + : ( is_array( $arg_value ) + ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) + : ( $arg_value === null + ? 'null' + : $wpdb->prepare( ( ! is_array( $value ) || ! array_key_exists( 'force_string', $value ) || ! $value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ) + ), + ]; + + // Between? + if ( is_array( $value ) && isset( $value['operator'] ) ) { + $value['operator'] = strtoupper( $value['operator'] ); + if ( strpos( $value['operator'], 'BETWEEN' ) !== false ) { + if ( array_key_exists( 'max', $value ) || array_key_exists( 'key_b', $value ) ) { + if ( array_key_exists( 'max', $value ) ) { + $arg_value = $value['max']; + } + if ( array_key_exists( 'sanitize_callback2', $value ) ) { + $sanitize_callback = $value['sanitize_callback2']; + } + if ( $sanitize_callback && ! array_key_exists( 'key_b', $value ) ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + $statement[] = 'AND'; + $statement[] = array_key_exists( 'key_b', $value ) + ? $value['key_b'] + : ( is_array( $arg_value ) + ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) + : $wpdb->prepare( ( ! array_key_exists( 'force_string', $value ) || ! $value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ); + } else { + throw new Exception( '"max" or "key_b "parameter must be indicated when using the BETWEEN operator.', 10202 ); + } + } + } + $this->builder['where'][] = [ + 'joint' => is_array( $value ) && isset( $value['joint'] ) ? $value['joint'] : 'AND', + 'condition' => $this->buildStatement( $statement ), + ]; + // Options - reset + if ( is_array( $value ) && array_key_exists( 'wildcard', $value ) && ! empty( $value['wildcard'] ) ) { + $this->options['wildcard'] = $this->options['default_wildcard']; + } + } + + return $this; + } + + /** + * Adds join statement. + * + * @param string $table Join table. + * @param array $args Join arguments. + * @param bool|string $type Flag that indicates if it is "LEFT or INNER", also accepts direct join string. + * @param bool $add_prefix Should DB prefix be added. + * + * @return QueryBuilder this for chaining. + * @throws Exception + * + * @since 1.0.0 + * + */ + public function join( $table, $args, $type = false, $add_prefix = true ) { + $type = is_string( $type ) ? strtoupper( trim( $type ) ) : ( $type ? 'LEFT' : '' ); + if ( ! in_array( $type, [ '', 'LEFT', 'RIGHT', 'INNER', 'CROSS', 'LEFT OUTER', 'RIGHT OUTER' ] ) ) { + throw new Exception( 'Invalid join type.', 10201 ); + } + global $wpdb; + $join = [ + 'table' => ( $add_prefix ? $wpdb->prefix : '' ) . $table, + 'type' => $type, + 'on' => [], + ]; + foreach ( $args as $argument ) { + // Options - set + if ( array_key_exists( 'wildcard', $argument ) && ! empty( $argument['wildcard'] ) ) { + $this->options['wildcard'] = trim( $argument['wildcard'] ); + } + // Value + $arg_value = isset( $argument['value'] ) ? $argument['value'] : null; + if ( array_key_exists( 'min', $argument ) ) { + $arg_value = $argument['min']; + } + $sanitize_callback = array_key_exists( 'sanitize_callback', $argument ) ? $argument['sanitize_callback'] : true; + if ( $sanitize_callback + && ! array_key_exists( 'raw', $argument ) + && ! array_key_exists( 'key_b', $argument ) + ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + $statement = array_key_exists( 'raw', $argument ) + ? [ $argument['raw'] ] + : [ + isset( $argument['key_a'] ) ? $argument['key_a'] : $argument['key'], + isset( $argument['operator'] ) ? strtoupper( $argument['operator'] ) : ( $arg_value === null && ! isset( $argument['key_b'] ) ? 'is' : '=' ), + array_key_exists( 'key_b', $argument ) + ? $argument['key_b'] + : ( is_array( $arg_value ) + ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) + : ( $arg_value === null + ? 'null' + : $wpdb->prepare( ( ! array_key_exists( 'force_string', $argument ) || ! $argument['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ) + ), + ]; + // Between? + if ( isset( $argument['operator'] ) ) { + $argument['operator'] = strtoupper( $argument['operator'] ); + if ( strpos( $argument['operator'], 'BETWEEN' ) !== false ) { + if ( array_key_exists( 'max', $argument ) || array_key_exists( 'key_c', $argument ) ) { + if ( array_key_exists( 'max', $argument ) ) { + $arg_value = $argument['max']; + } + if ( array_key_exists( 'sanitize_callback2', $argument ) ) { + $sanitize_callback = $argument['sanitize_callback2']; + } + if ( $sanitize_callback && ! array_key_exists( 'key_c', $argument ) ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + $statement[] = 'AND'; + $statement[] = array_key_exists( 'key_c', $argument ) + ? $argument['key_c'] + : ( is_array( $arg_value ) + ? ( '(\'' . implode( '\',\'', $arg_value ) . '\')' ) + : $wpdb->prepare( ( ! array_key_exists( 'force_string', $argument ) || ! $argument['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ); + } else { + throw new Exception( '"max" or "key_c" parameter must be indicated when using the BETWEEN operator.', 10203 ); + } + } + } + $join['on'][] = [ + 'joint' => isset( $argument['joint'] ) ? $argument['joint'] : 'AND', + 'condition' => implode( ' ', $statement ), + ]; + // Options - reset + if ( array_key_exists( 'wildcard', $argument ) && ! empty( $argument['wildcard'] ) ) { + $this->options['wildcard'] = $this->options['default_wildcard']; + } + } + $this->builder['join'][] = $join; + + return $this; + } + + /** + * Adds limit statement. + * + * @param int $limit + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + * + */ + public function limit( $limit ) { + $this->builder['limit'] = $limit; + + return $this; + } + + /** + * Adds offset statement. + * + * @param int $offset + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + * + */ + public function offset( $offset ) { + $this->builder['offset'] = $offset; + + return $this; + } + + /** + * Adds order by statement. + * + * @param string $key + * @param string $direction + * + * @return QueryBuilder this for chaining. + * @throws Exception + * @since 1.0.0 + * + */ + public function order_by( $key, $direction = 'ASC' ) { + $direction = trim( strtoupper( $direction ) ); + if ( $direction !== 'ASC' && $direction !== 'DESC' ) { + throw new Exception( 'Invalid direction value.', 10200 ); + } + if ( ! empty( $key ) ) { + $this->builder['order'][] = $key . ' ' . $direction; + } + + return $this; + } + + /** + * Adds group by statement. + * + * @param string $statement + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + * + */ + public function group_by( $statement ) { + if ( ! empty( $statement ) ) { + $this->builder['group'][] = $statement; + } + + return $this; + } + + /** + * Adds having statement. + * + * @param string $statement + * + * @return QueryBuilder this for chaining. + * @since 1.0.0 + * + */ + public function having( $statement ) { + if ( ! empty( $statement ) ) { + $this->builder['having'] = $statement; + } + + return $this; + } + + /** + * Adds set statement (for update). + * + * @param array $args Multiple where arguments. + * + * @return QueryBuilder this for chaining. + * @since 1.0.12 + * + */ + + /** + * Adds set statement (for update). + * + * @param array $args Multiple where arguments. + * + * @return QueryBuilder this for chaining. + * @since 1.0.12 + * @since 1.1.0 Modified line 39 to wrap the key in `..` + * + * @global object $wpdb + * + */ + public function set( $args ) { + + global $wpdb; + foreach ( $args as $key => $value ) { + // Value + $arg_value = is_array( $value ) && array_key_exists( 'value', $value ) ? $value['value'] : $value; + $sanitize_callback = is_array( $value ) && array_key_exists( 'sanitize_callback', $value ) + ? $value['sanitize_callback'] + : true; + if ( $sanitize_callback + && $key !== 'raw' + && ( ! is_array( $value ) || ! array_key_exists( 'raw', $value ) ) + ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + $statement = $key === 'raw' + ? [ $arg_value ] + : [ + sprintf( '%s', $key ), + '=', + is_array( $value ) && array_key_exists( 'raw', $value ) + ? $value['raw'] + : ( is_array( $arg_value ) + ? ( '\'' . implode( ',', $arg_value ) . '\'' ) + : ( $arg_value === null + ? 'null' + : $wpdb->prepare( ( ! is_array( $value ) || ! array_key_exists( 'force_string', $value ) || ! $value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ) + ), + ]; + $this->builder['set'][] = $this->buildStatement( $statement ); + } + + return $this; + } + + /** + * Adds values statement + * + * @param $args + * + * @return $this + */ + public function values( $args ) { + global $wpdb; + + if ( ! isset( $this->builder['values'] ) ) { + $this->builder['values'] = []; + } + + foreach ( $args as $key => $value ) { + // Value + $arg_value = is_array( $value ) && array_key_exists( 'value', $value ) ? $value['value'] : $value; + $sanitize_callback = is_array( $value ) && array_key_exists( 'sanitize_callback', $value ) + ? $value['sanitize_callback'] + : true; + if ( $sanitize_callback + && $key !== 'raw' + && ( ! is_array( $value ) || ! array_key_exists( 'raw', $value ) ) + ) { + $arg_value = $this->sanitize_value( $sanitize_callback, $arg_value ); + } + + $preparedKey = sprintf( '%s', $key ); + + if ( is_array( $value ) && array_key_exists( 'raw', $value ) ) { + $this->builder['values'][ $preparedKey ] = $value['raw']; + } else { + if ( is_array( $arg_value ) ) { + $this->builder['values'][ $preparedKey ] = ( '\'' . implode( ',', $arg_value ) . '\'' ); + } else { + $this->builder['values'][ $preparedKey ] = ( $arg_value === null + ? 'null' + : $wpdb->prepare( ( ! is_array( $value ) || ! array_key_exists( 'force_string', $value ) || ! $value['force_string'] ) && is_numeric( $arg_value ) ? '%d' : '%s', $arg_value ) + ); + } + } + } + + return $this; + } + + /** + * Retunrs results from builder statements. + * + * @param int $output WPDB output type. + * @param callable $callable_mapping Function callable to filter or map results to. + * @param bool $calc_rows Flag that indicates to SQL if rows should be calculated or not. + * + * @return array + * @since 1.0.0 + * + */ + public function get( $output = OBJECT, $callable_mapping = null, $calc_rows = false ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_get_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_get_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_select( $query, $calc_rows ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + $this->_query_order( $query ); + $this->_query_limit( $query ); + $this->_query_offset( $query ); + // Process + $query = apply_filters( 'query_builder_get_query', $query ); + $query = apply_filters( 'query_builder_get_query_' . $this->id, $query ); + $results = $wpdb->get_results( $query, $output ); + if ( $callable_mapping ) { + $results = array_map( function ( $row ) use ( &$callable_mapping ) { + return call_user_func_array( $callable_mapping, [ $row ] ); + }, $results ); + } + + return $results; + } + + /** + * Returns first row found. + * + * @param int $output WPDB output type. + * + * @return object|array + * @since 1.0.0 + * + */ + public function first( $output = OBJECT ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_first_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_first_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_select( $query ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + $this->_query_order( $query ); + $query .= ' LIMIT 1'; + $this->_query_offset( $query ); + // Process + $query = apply_filters( 'query_builder_first_query', $query ); + $query = apply_filters( 'query_builder_first_query_' . $this->id, $query ); + + return $wpdb->get_row( $query, $output ); + } + + /** + * Returns a value. + * + * @param int $x Column of value to return. Indexed from 0. + * @param int $y Row of value to return. Indexed from 0. + * + * @return mixed + * + * @since 1.0.0 + * + */ + public function value( $x = 0, $y = 0 ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_value_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_value_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_select( $query ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + $this->_query_order( $query ); + $this->_query_limit( $query ); + $this->_query_offset( $query ); + // Process + $query = apply_filters( 'query_builder_value_query', $query ); + $query = apply_filters( 'query_builder_value_query_' . $this->id, $query ); + + return $wpdb->get_var( $query, $x, $y ); + } + + /** + * Returns the count. + * + * @param string|int $column Count column. + * @param bool $bypass_limit Flag that indicates if limit + offset should be considered on count. + * + * @return int + * + * @since 1.0.0 + * + */ + public function count( $column = 1, $bypass_limit = true ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_count_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_count_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = 'SELECT count(' . $column . ') as `count`'; + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + if ( ! $bypass_limit ) { + $this->_query_limit( $query ); + $this->_query_offset( $query ); + } + // Process + $query = apply_filters( 'query_builder_count_query', $query ); + $query = apply_filters( 'query_builder_count_query_' . $this->id, $query ); + + return intval( $wpdb->get_var( $query ) ); + } + + /** + * Returns column results from builder statements. + * + * @param int $x Column index number. + * @param bool $calc_rows Flag that indicates to SQL if rows should be calculated or not. + * + * @return array + * + * @since 1.0.6 + * + */ + public function col( $x = 0, $calc_rows = false ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_col_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_col_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_select( $query, $calc_rows ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + $this->_query_order( $query ); + $this->_query_limit( $query ); + $this->_query_offset( $query ); + // Process + $query = apply_filters( 'query_builder_col_query', $query ); + $query = apply_filters( 'query_builder_col_query_' . $this->id, $query ); + + return $wpdb->get_col( $query, $x ); + } + + /** + * Returns flag indicating if query has been executed. + * + * @param string $sql + * + * @return bool + * @since 1.0.8 + * + */ + public function query( $sql = '' ) { + global $wpdb; + $this->builder = apply_filters( 'query_builder_query_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_query_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = $sql; + if ( empty( $query ) ) { + $this->_query_select( $query ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + $this->_query_group( $query ); + $this->_query_having( $query ); + $this->_query_order( $query ); + $this->_query_limit( $query ); + $this->_query_offset( $query ); + } + // Process + $query = apply_filters( 'query_builder_query_query', $query ); + $query = apply_filters( 'query_builder_query_query_' . $this->id, $query ); + + return $wpdb->query( $query ); + } + + /** + * Returns flag indicating if query has been executed. + * + * @param string $sql + * + * @return bool + * @since 1.0.8 + * + * @see self::query() + * + */ + public function raw( $sql ) { + return $this->query( $sql ); + } + + /** + * Returns flag indicating if delete query has been executed. + * @return bool + * + * @since 1.0.8 + * + */ + public function delete() { + global $wpdb; + $this->builder = apply_filters( 'query_builder_delete_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_delete_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_delete( $query ); + $this->_query_from( $query ); + $this->_query_join( $query ); + $this->_query_where( $query ); + // Process + $query = apply_filters( 'query_builder_delete_query', $query ); + $query = apply_filters( 'query_builder_delete_query_' . $this->id, $query ); + + return $wpdb->query( $query ); + } + + /** + * The insert query + * @since 1.1.0 + * @return bool|int|\mysqli_result|resource|null + */ + public function insert() { + global $wpdb; + $this->builder = apply_filters( 'query_builder_insert_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_insert_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_insert( $query ); + // Process + $query = apply_filters( 'query_builder_insert_query', $query ); + $query = apply_filters( 'query_builder_insert_query_' . $this->id, $query ); + + $result = $wpdb->query( $query ); + + if ( false !== $result ) { + return $wpdb->insert_id; + } else { + return null; + } + } + + /** + * Returns flag indicating if update query has been executed. + * @return bool + * + * @since 1.0.12 + * + */ + public function update() { + global $wpdb; + $this->builder = apply_filters( 'query_builder_update_builder', $this->builder ); + $this->builder = apply_filters( 'query_builder_update_builder_' . $this->id, $this->builder ); + // Build + // Query + $query = ''; + $this->_query_update( $query ); + $this->_query_join( $query ); + $this->_query_set( $query ); + $this->_query_where( $query ); + // Process + $query = apply_filters( 'query_builder_update_query', $query ); + $query = apply_filters( 'query_builder_update_query_' . $this->id, $query ); + + return $wpdb->query( $query ); + } + + /** + * Retunrs found rows in last query, if SQL_CALC_FOUND_ROWS is used and is supported. + * @return array + * + * @since 1.0.6 + * + */ + public function rows_found() { + global $wpdb; + $query = 'SELECT FOUND_ROWS()'; + // Process + $query = apply_filters( 'query_builder_found_rows_query', $query ); + $query = apply_filters( 'query_builder_found_rows_query_' . $this->id, $query ); + + return $wpdb->get_var( $query ); + } + + /** + * Builds query's select statement. + * + * @param string &$query + * @param bool $calc_rows + * + * @since 1.0.0 + * + */ + private function _query_select( &$query, $calc_rows = false ) { + $query = 'SELECT ' . ( $calc_rows ? 'SQL_CALC_FOUND_ROWS ' : '' ) . ( + is_array( $this->builder['select'] ) && count( $this->builder['select'] ) + ? implode( ',', $this->builder['select'] ) + : '*' + ); + } + + /** + * Builds query's from statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_from( &$query ) { + $query .= ' FROM ' . $this->builder['from']; + } + + /** + * Builds query's join statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_join( &$query ) { + foreach ( $this->builder['join'] as $join ) { + $query .= ( ! empty( $join['type'] ) ? ' ' . $join['type'] . ' JOIN ' : ' JOIN ' ) . $join['table']; + for ( $i = 0; $i < count( $join['on'] ); ++ $i ) { + $query .= ( $i === 0 ? ' ON ' : ' ' . $join['on'][ $i ]['joint'] . ' ' ) + . $join['on'][ $i ]['condition']; + } + } + } + + /** + * Builds query's where statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_where( &$query ) { + for ( $i = 0; $i < count( $this->builder['where'] ); ++ $i ) { + $query .= ( $i === 0 ? ' WHERE ' : ' ' . $this->builder['where'][ $i ]['joint'] . ' ' ) + . $this->builder['where'][ $i ]['condition']; + } + } + + /** + * Builds query's group by statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_group( &$query ) { + if ( count( $this->builder['group'] ) ) { + $query .= ' GROUP BY ' . implode( ',', $this->builder['group'] ); + } + } + + /** + * Builds query's having statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_having( &$query ) { + if ( $this->builder['having'] ) { + $query .= ' HAVING ' . $this->builder['having']; + } + } + + /** + * Builds query's order by statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_order( &$query ) { + if ( count( $this->builder['order'] ) ) { + $query .= ' ORDER BY ' . implode( ',', $this->builder['order'] ); + } + } + + /** + * Builds query's limit statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_limit( &$query ) { + global $wpdb; + if ( $this->builder['limit'] ) { + $query .= $wpdb->prepare( ' LIMIT %d', $this->builder['limit'] ); + } + } + + /** + * Builds query's offset statement. + * + * @param string &$query + * + * @since 1.0.0 + * + */ + private function _query_offset( &$query ) { + global $wpdb; + if ( $this->builder['offset'] ) { + $query .= $wpdb->prepare( ' OFFSET %d', $this->builder['offset'] ); + } + } + + /** + * Builds query's delete statement. + * + * @param string &$query + * + * @since 1.0.8 + * + */ + private function _query_delete( &$query ) { + $query .= trim( 'DELETE ' . ( count( $this->builder['join'] ) + ? preg_replace( '/\s[aA][sS][\s\S]+.*?/', '', $this->builder['from'] ) + : '' + ) ); + } + + /** + * Prepares the insert query + * + * @param $query + * @since 1.1.0 + * + * @return void + */ + protected function _query_insert( &$query ) { + $query = trim( 'INSERT INTO ' . $this->builder['from'] . ' ' . '(' . implode( ', ', array_keys( $this->builder['values'] ) ) . ')' . ' VALUES(' . implode( ', ', array_values( $this->builder['values'] ) ) . ')' ); + } + + /** + * Builds query's update statement. + * + * @param string &$query + * + * @since 1.0.12 + * + */ + private function _query_update( &$query ) { + $query .= trim( 'UPDATE ' . ( count( $this->builder['join'] ) + ? $this->builder['from'] . ',' . implode( ',', array_map( function ( $join ) { + return $join['table']; + }, $this->builder['join'] ) ) + : $this->builder['from'] + ) ); + } + + /** + * Builds query's set statement. + * + * @param string &$query + * + * @since 1.0.12 + * + */ + private function _query_set( &$query ) { + $query .= $this->builder['set'] ? ' SET ' . implode( ',', $this->builder['set'] ) : ''; + } + + /** + * Sanitize value. + * + * @param string|bool $callback Sanitize callback. + * @param mixed $value + * + * @return mixed + * @since 1.0.0 + * + */ + private function sanitize_value( $callback, $value ) { + if ( $callback === true ) { + $callback = ( is_numeric( $value ) && strpos( $value, '.' ) !== false ) + ? 'floatval' + : ( is_numeric( $value ) + ? 'intval' + : ( is_string( $value ) + ? 'sanitize_text_field' + : null + ) + ); + } + if ( $callback && strpos( $callback, '_builder' ) !== false ) { + $callback = [ &$this, $callback ]; + } + if ( is_array( $value ) ) { + for ( $i = count( $value ) - 1; $i >= 0; -- $i ) { + $value[ $i ] = $this->sanitize_value( true, $value[ $i ] ); + } + } + + return $callback && is_callable( $callback ) ? call_user_func_array( $callback, [ $value ] ) : $value; + } + + /** + * Build statement + * + * @param array $statement + * + * @return string + */ + protected function buildStatement( $statement ) { + $imploded = implode( ' ', $statement ); + + return str_replace( [ "'NOT NULL'", "'not null'", "'NULL'", "'null'" ], [ "NOT NULL", "not nulll", "NULL", "null" ], $imploded ); + } + + /** + * Returns value escaped with WPDB `esc_like`, + * + * @param mixed $value + * + * @return string + * @since 1.0.6 + * + */ + private function _builder_esc_like( $value ) { + global $wpdb; + $wildcard = $this->options['wildcard']; + + return implode( '%', array_map( function ( $part ) use ( &$wpdb, &$wildcard ) { + return $wpdb->esc_like( $part ); + }, explode( $wildcard, $value ) ) ); + } + + /** + * Returns escaped value for LIKE comparison and appends wild card at the beggining. + * + * @param mixed $value + * + * @return string + * @since 1.0.6 + * + */ + private function _builder_esc_like_wild_value( $value ) { + return '%' . $this->_builder_esc_like( $value ); + } + + /** + * Returns escaped value for LIKE comparison and appends wild card at the end. + * + * @param mixed $value + * + * @return string + * @since 1.0.6 + * + */ + private function _builder_esc_like_value_wild( $value ) { + return $this->_builder_esc_like( $value ) . '%'; + } + + /** + * Returns escaped value for LIKE comparison and appends wild cards at both ends. + * + * @param mixed $value + * + * @return string + * @since 1.0.6 + * + */ + private function _builder_esc_like_wild_wild( $value ) { + return '%' . $this->_builder_esc_like( $value ) . '%'; + } } \ No newline at end of file diff --git a/src/Traits/DataModelTrait.php b/src/Traits/DataModelTrait.php deleted file mode 100644 index c11296b..0000000 --- a/src/Traits/DataModelTrait.php +++ /dev/null @@ -1,175 +0,0 @@ - - * @license MIT - * @package wp-query-builder - * @version 1.0.12 - */ -trait DataModelTrait -{ - /** - * Static constructor that finds recond in database - * and fills model. - * @since 1.0.0 - * - * @param mixed $id - * - * @return \TenQuality\WP\Database\Abstracts\DataModel|null - */ - public static function find( $id ) - { - $model = new self( [], $id ); - return $model->load(); - } - /** - * Static constructor that finds recond in database - * and fills model using where statement. - * @since 1.0.0 - * - * @param array $args Where query statement arguments. See non-static method. - * - * @return \TenQuality\WP\Database\Abstracts\DataModel - */ - public static function find_where( $args ) - { - $model = new self; - return $model->load_where( $args ); - } - /** - * Static constructor that inserts recond in database and fills model. - * @since 1.0.0 - * - * @param array $attributes - * - * @return \TenQuality\WP\Database\Abstracts\DataModel - */ - public static function insert( $attributes ) - { - $model = new self( $attributes ); - return $model->save( true ) ? $model : null; - } - /** - * Static constructor that deletes records - * @since 1.0.0 - * - * @param array $args Where query statement arguments. See non-static method. - * - * @return bool - */ - public static function delete_where( $args ) - { - $model = new self; - return $model->_delete_where( $args ); - } - /** - * Returns a collection of models. - * @since 1.0.0 - * - * @return array - */ - public static function where( $args = [] ) - { - // Pull specific data from args - $limit = isset( $args['limit'] ) ? $args['limit'] : null; - unset( $args['limit'] ); - $offset = isset( $args['offset'] ) ? $args['offset'] : 0; - unset( $args['offset'] ); - $keywords = isset( $args['keywords'] ) ? $args['keywords'] : null; - unset( $args['keywords'] ); - $keywords_separator = isset( $args['keywords_separator'] ) ? $args['keywords_separator'] : ' '; - unset( $args['keywords_separator'] ); - $order_by = isset( $args['order_by'] ) ? $args['order_by'] : null; - unset( $args['order_by'] ); - $order = isset( $args['order'] ) ? $args['order'] : 'ASC'; - unset( $args['order'] ); - // Build query and retrieve - $builder = new QueryBuilder( self::TABLE . '_where' ); - return array_map( - function( $attributes ) { - return new self( $attributes ); - }, - $builder->select( '*' ) - ->from( self::TABLE . ' as `' . self::TABLE . '`' ) - ->keywords( $keywords, static::$keywords, $keywords_separator ) - ->where( $args ) - ->order_by( $order_by, $order ) - ->limit( $limit ) - ->offset( $offset ) - ->get( ARRAY_A ) - ); - } - /** - * Returns count. - * @since 1.0.0 - * - * @return int - */ - public static function count( $args = [] ) - { - // Pull specific data from args - unset( $args['limit'] ); - unset( $args['offset'] ); - $keywords = isset( $args['keywords'] ) ? sanitize_text_field( $args['keywords'] ) : null; - unset( $args['keywords'] ); - // Build query and retrieve - $builder = new QueryBuilder( self::TABLE . '_count' ); - return $builder->from( self::TABLE . ' as `' . self::TABLE . '`' ) - ->keywords( $keywords, static::$keywords ) - ->where( $args ) - ->count(); - } - /** - * Returns initialized builder with model set in from statement. - * @since 1.0.0 - * - * @return \TenQuality\WP\Database\Utility\QueryBuilder - */ - public static function builder() - { - $builder = new QueryBuilder( self::TABLE . '_custom' ); - return $builder->from( self::TABLE . ' as `' . self::TABLE . '`' ); - } - /** - * Returns a collection with all models found in the database. - * @since 1.0.7 - * - * @return array - */ - public static function all() - { - // Build query and retrieve - $builder = new QueryBuilder( self::TABLE . '_all' ); - return array_map( - function( $attributes ) { - return new self( $attributes ); - }, - $builder->select( '*' ) - ->from( self::TABLE . ' as `' . self::TABLE . '`' ) - ->get( ARRAY_A ) - ); - } - /** - * Returns query results from mass update. - * @since 1.0.12 - * - * @param array $set Set of column => data to update. - * @param array $where Where condition. - * - * @return \TenQuality\WP\Database\Abstracts\DataModel|null - */ - public static function update_all( $set, $where = [] ) - { - $builder = new QueryBuilder( self::TABLE . '_static_update' ); - return $builder->from( self::TABLE ) - ->set( $set ) - ->where( $where ) - ->update(); - } -} \ No newline at end of file diff --git a/tests/cases/AbstractModelTest.php b/tests/cases/AbstractModelTest.php index 7e6c136..23348d4 100644 --- a/tests/cases/AbstractModelTest.php +++ b/tests/cases/AbstractModelTest.php @@ -6,6 +6,7 @@ * Test. * * @author 10 Quality + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/FunctionsTest.php b/tests/cases/FunctionsTest.php index 9e98aa6..9d234ad 100644 --- a/tests/cases/FunctionsTest.php +++ b/tests/cases/FunctionsTest.php @@ -1,12 +1,13 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.9 diff --git a/tests/cases/HooksTest.php b/tests/cases/HooksTest.php index 79dd61d..f67bb92 100644 --- a/tests/cases/HooksTest.php +++ b/tests/cases/HooksTest.php @@ -6,6 +6,7 @@ * Test. * * @author 10 Quality + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/QueryBuilderConditionsTest.php b/tests/cases/QueryBuilderConditionsTest.php index 255e4b2..5a630b2 100644 --- a/tests/cases/QueryBuilderConditionsTest.php +++ b/tests/cases/QueryBuilderConditionsTest.php @@ -1,12 +1,13 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/QueryBuilderOperationsTest.php b/tests/cases/QueryBuilderOperationsTest.php index 1e8a430..d65c226 100644 --- a/tests/cases/QueryBuilderOperationsTest.php +++ b/tests/cases/QueryBuilderOperationsTest.php @@ -1,12 +1,13 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/QueryBuilderStatementsTest.php b/tests/cases/QueryBuilderStatementsTest.php index 19b5605..13892cc 100644 --- a/tests/cases/QueryBuilderStatementsTest.php +++ b/tests/cases/QueryBuilderStatementsTest.php @@ -1,12 +1,13 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/SanitizationTest.php b/tests/cases/SanitizationTest.php index 49ba143..399cfa8 100644 --- a/tests/cases/SanitizationTest.php +++ b/tests/cases/SanitizationTest.php @@ -1,12 +1,13 @@ + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/cases/TraitModelTest.php b/tests/cases/TraitModelTest.php index 281fc6e..57171d0 100644 --- a/tests/cases/TraitModelTest.php +++ b/tests/cases/TraitModelTest.php @@ -6,6 +6,7 @@ * Test. * * @author 10 Quality + * @author Darko G. * @license MIT * @package wp-query-builder * @version 1.0.13 diff --git a/tests/framework/class.model.php b/tests/framework/class.model.php index af69dd4..c804490 100644 --- a/tests/framework/class.model.php +++ b/tests/framework/class.model.php @@ -1,11 +1,9 @@