first commit

This commit is contained in:
Ryan Ariana
2024-05-06 11:04:37 +07:00
commit aee061ddba
7322 changed files with 2918816 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
<?php
namespace ElementorPro\Core\Database;
use ElementorPro\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Database_Updater {
/**
* Run all the 'up' method of the migrations classes if needed, and update the db version.
*
* @param bool $force When passing true, it ignores the current version and run all the up migrations.
*/
public function up( $force = false ) {
$installed_version = $this->get_installed_version();
// Up to date. Nothing to do.
if ( ! $force && $this->get_db_version() <= $installed_version ) {
return;
}
$migrations = $this->get_collected_migrations();
if ( ! $force ) {
$migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) {
// Filter all the migrations that already done.
return $version > $installed_version;
} );
}
$migrations->map( function ( Base_Migration $migration, $version ) {
$migration->up();
// In case some migration failed it updates version every migration.
$this->update_db_version_option( $version );
} );
$this->update_db_version_option( $this->get_db_version() );
}
/**
* Run all the 'down' method of the migrations classes if can, and update the db version.
*
* @param bool $force When passing true, it ignores the current version and run all the down migrations.
*/
public function down( $force = false ) {
$installed_version = $this->get_installed_version();
$migrations = $this->get_collected_migrations();
if ( ! $force ) {
$migrations = $migrations->filter( function ( $_, $version ) use ( $installed_version ) {
// Filter all the migrations that was not installed.
return $version <= $installed_version;
} );
}
$migrations->reverse( true )
->map( function ( Base_Migration $migration, $version ) {
$migration->down();
// In case some migration failed it updates version every migration.
$this->update_db_version_option( $version );
} );
$this->update_db_version_option( 0 );
}
/**
* Register hooks to activate the migrations.
*/
public function register() {
add_action( 'admin_init', function () {
$this->up();
} );
}
/**
* Update the version in the users DB.
*
* @param $version
*/
protected function update_db_version_option( $version ) {
update_option( $this->get_db_version_option_name(), $version );
}
/**
* Get the version that already installed.
*
* @return int
*/
protected function get_installed_version() {
return intval( get_option( $this->get_db_version_option_name() ) );
}
/**
* Get all migrations inside a Collection.
*
* @return Collection
*/
protected function get_collected_migrations() {
return new Collection( $this->get_migrations() );
}
/**
* The most updated version of the DB.
*
* @return numeric
*/
abstract protected function get_db_version();
/**
* The name of the option that saves the current user DB version.
*
* @return string
*/
abstract protected function get_db_version_option_name();
/**
* Array of migration classes.
*
* @return Base_Migration[]
*/
abstract protected function get_migrations();
}

View File

@@ -0,0 +1,182 @@
<?php
namespace ElementorPro\Core\Database;
use Elementor\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Migration {
/*
* @see https://github.com/WordPress/WordPress/blob/d2694aa46647af48d1bcaff48a4f6cac7f5cf470/wp-admin/includes/schema.php#L49
*/
const MAX_INDEX_LENGTH = 191;
/**
* @var \wpdb
*/
protected $wpdb;
/**
* @param \wpdb|null $wpdb_instance
*/
public function __construct( \wpdb $wpdb_instance = null ) {
if ( ! $wpdb_instance ) {
global $wpdb;
$this->wpdb = $wpdb;
} else {
$this->wpdb = $wpdb_instance;
}
}
/**
* Runs when upgrading the database
*
* @return void
*/
abstract public function up();
/**
* Runs when downgrading the database.
*
* @return void
*/
abstract public function down();
/**
* A util to run SQL for creating tables.
*
* @param $table_name
* @param array $columns
*/
protected function create_table( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$columns_sql = ( new Collection( $columns ) )
->map( function( $definition, $col_name ) {
return "`{$col_name}` {$definition}";
} )
->implode( ', ' );
$query = "CREATE TABLE `{$table_name}` ({$columns_sql}) {$this->wpdb->get_charset_collate()};";
$this->run_db_delta( $query );
}
/**
* Add columns.
*
* @param $table_name
* @param array $columns
*/
protected function add_columns( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$add_columns_sql = ( new Collection( $columns ) )
->map( function ( $definition, $column_name ) {
return "ADD COLUMN `{$column_name}` {$definition}";
} )
->implode( ', ' );
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$add_columns_sql};" ); // phpcs:ignore
}
/**
* Drop columns
*
* @param $table_name
* @param array $columns
*/
protected function drop_columns( $table_name, array $columns ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$drop_columns_sql = ( new Collection( $columns ) )
->map( function ( $column_name ) {
return "DROP COLUMN `{$column_name}`";
} )
->implode( ', ' );
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$drop_columns_sql};" ); // phpcs:ignore
}
/**
* A util to run SQL for dropping tables.
*
* @param $table_name
*/
protected function drop_table( $table_name ) {
$table_name = "{$this->wpdb->prefix}{$table_name}";
$query = "DROP TABLE IF EXISTS `{$table_name}`;";
// Safe query that shouldn't be escaped.
$this->wpdb->query( $query ); // phpcs:ignore
}
/**
* A util to run SQL for creating indexes.
*
* @param $table_name
* @param array $column_names
*/
protected function create_indexes( $table_name, array $column_names ) {
$max_index_length = static::MAX_INDEX_LENGTH;
$table_name = "{$this->wpdb->prefix}{$table_name}";
// Safe query that shouldn't be escaped.
$column_definition = $this->get_column_definition( $table_name );
if ( ! $column_definition ) {
return;
}
$should_set_max_length_index = ( new Collection( $column_definition ) )
->filter( function ( $value ) {
preg_match( '/\((\d+)\)/', $value['Type'], $match );
return ( isset( $match[1] ) && intval( $match[1] ) > Base_Migration::MAX_INDEX_LENGTH )
|| in_array( strtolower( $value['Type'] ), [ 'text', 'longtext' ], true );
} )
->pluck( 'Field' )
->values();
$indexes_sql = ( new Collection( $column_names ) )
->map( function( $col_name ) use ( $should_set_max_length_index, $max_index_length ) {
$max_index_length_sql = '';
if ( in_array( $col_name, $should_set_max_length_index, true ) ) {
$max_index_length_sql = " ({$max_index_length})";
}
return "ADD INDEX `{$col_name}_index` (`{$col_name}`{$max_index_length_sql})";
} )
->implode( ', ' );
// Safe query that shouldn't be escaped.
$this->wpdb->query( "ALTER TABLE `{$table_name}` {$indexes_sql};" ); // phpcs:ignore
}
/**
* @param $table_name
*
* @return array
*/
protected function get_column_definition( $table_name ) {
return $this->wpdb->get_results( "SHOW COLUMNS FROM `{$table_name}`;", ARRAY_A ); // phpcs:ignore
}
/**
* Runs global dbDelta function (wrapped into method to allowing mock for testing).
*
* @param $query
*
* @return array
*/
protected function run_db_delta( $query ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
return dbDelta( $query );
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace ElementorPro\Core\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* JOIN clause builder.
*
* Essentially, it uses the regular Builder's capabilities while wrapping some method
* for syntactic sugar and better readability.
*/
class Join_Clause extends Query_Builder {
// JOIN types.
const TYPE_INNER = 'inner';
const TYPE_LEFT = 'left';
const TYPE_RIGHT = 'right';
/**
* JOIN type.
*
* @var string
*/
public $type;
/**
* Join_Clause constructor.
*
* @param string $type - JOIN type.
* @param \wpdb|null $connection - MySQL connection to use.
*
* @return void
*/
public function __construct( $type, \wpdb $connection = null ) {
parent::__construct( $connection );
$this->type = $type;
}
/**
* @uses `$this->where()`.
*
* @return Join_Clause
*/
public function on( $column, $operator, $value, $and_or = self::RELATION_AND ) {
return $this->where( $column, $operator, $value, $and_or );
}
/**
* @shortcut `$this->on()`.
*
* @return Join_Clause
*/
public function or_on( $first, $operator, $second ) {
return $this->on( $first, $operator, $second, self::RELATION_OR );
}
/**
* @uses `$this->where_column()`.
*
* @return Join_Clause
*/
public function on_column( $first, $operator, $second, $and_or = self::RELATION_AND ) {
return $this->where_column( $first, $operator, $second, $and_or );
}
/**
* @shortcut `$this->on_column()`.
*
* @return Join_Clause
*/
public function or_on_column( $first, $operator, $second ) {
return $this->on_column( $first, $operator, $second, self::RELATION_OR );
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace ElementorPro\Core\Database;
use ElementorPro\Core\Utils\Collection;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Model_Base implements \JsonSerializable {
// Casting types.
const TYPE_BOOLEAN = 'boolean';
const TYPE_COLLECTION = 'collection';
const TYPE_INTEGER = 'integer';
const TYPE_STRING = 'string';
const TYPE_JSON = 'json';
const TYPE_DATETIME = 'datetime';
const TYPE_DATETIME_GMT = 'datetime_gmt';
/**
* Casts array.
* Used to automatically cast values from DB to the appropriate property type.
*
* @var array
*/
protected static $casts = [];
/**
* Model_Base constructor.
*
* @param array $fields - Fields from the DB to fill.
*
* @return void
*/
public function __construct( array $fields ) {
foreach ( $fields as $key => $value ) {
if ( ! property_exists( $this, $key ) ) {
continue;
}
$this->{$key} = ( empty( static::$casts[ $key ] ) )
? $value
: static::cast( $value, static::$casts[ $key ] );
}
}
/**
* Get the model's table name.
* Throws an exception by default in order to require implementation,
* since abstract static functions are not allowed.
*
* @return string
*/
public static function get_table() {
throw new \Exception( 'You must implement `get_table()` inside ' . static::class );
}
/**
* Create a Query Builder for the model's table.
*
* @param \wpdb|null $connection - MySQL connection to use.
*
* @return Query_Builder
*/
public static function query( \wpdb $connection = null ) {
$builder = new Model_Query_Builder( static::class, $connection );
return $builder->from( static::get_table() );
}
/**
* Cast value into specific type.
*
* @param $value - Value to cast.
* @param $type - Type to cast into.
*
* @return mixed
*/
protected static function cast( $value, $type ) {
if ( null === $value ) {
return null;
}
switch ( $type ) {
case self::TYPE_BOOLEAN:
return boolval( $value );
case self::TYPE_COLLECTION:
return new Collection( $value );
case self::TYPE_INTEGER:
return intval( $value );
case self::TYPE_STRING:
return strval( $value );
case self::TYPE_JSON:
return json_decode( $value, true );
case self::TYPE_DATETIME:
return new \DateTime( $value );
case self::TYPE_DATETIME_GMT:
return new \DateTime( $value, new \DateTimeZone( 'GMT' ) );
}
return $value;
}
/**
* Cast a model property value into a JSON compatible data type.
*
* @param $value - Value to cast.
* @param $type - Type to cast into.
* @param $property_name - The model property name.
*
* @return mixed
*/
protected static function json_serialize_property( $value, $type, $property_name ) {
switch ( $type ) {
case self::TYPE_DATETIME:
case self::TYPE_DATETIME_GMT:
/** @var \DateTime $value */
return $value->format( 'c' );
}
/** @var mixed $value */
return $value;
}
/**
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return ( new Collection( (array) $this ) )
->map( function ( $_, $key ) {
$value = $this->{$key};
$type = array_key_exists( $key, static::$casts )
? static::$casts[ $key ]
: null;
if ( null === $value ) {
return $value;
}
// Can be overridden by child model.
$value = static::json_serialize_property( $value, $type, $key );
if ( $value instanceof \JsonSerializable ) {
return $value->jsonSerialize();
}
return $value;
} )
->all();
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace ElementorPro\Core\Database;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Model_Query_Builder extends Query_Builder {
/**
* The Query Builder associated model.
*
* @var string
*/
public $model;
/**
* Whether the returned value should be hydrated into a model.
*
* @var bool
*/
public $return_as_model = true;
/**
* Model_Query_Builder constructor.
*
* @param string $model_classname - Model to use inside the builder.
* @param \wpdb|null $connection - MySQL connection.
*/
public function __construct( $model_classname, \wpdb $connection = null ) {
$this->set_model( $model_classname );
parent::__construct( $connection );
}
/**
* Set the model the generated from the query builder.
*
* @param $model_classname
*
* @return $this
*/
public function set_model( $model_classname ) {
$this->model = $model_classname;
return $this;
}
/**
* Disable model hydration.
*
* @return $this
*/
public function disable_model_initiation() {
$this->return_as_model = false;
return $this;
}
/**
* Disable hydration before calling the original count.
*
* @param string $column
*
* @return int
*/
public function count( $column = '*' ) {
$this->disable_model_initiation();
return parent::count( $column );
}
/**
* Disable hydration before calling the original pluck.
*
* @inheritDoc
*/
public function pluck( $column = null ) {
$this->disable_model_initiation();
return parent::pluck( $column );
}
/**
* Override the parent `get()` and make Models from the results.
*
* @return \ElementorPro\Core\Utils\Collection
*/
public function get() {
$items = parent::get();
if ( ! $this->return_as_model ) {
return $items;
}
// Convert the SQL results to Model instances.
return $items->map( function ( $comment ) {
return new $this->model( $comment );
} );
}
}

File diff suppressed because it is too large Load Diff