File: //proc/self/cwd/wp-content/plugins/elementor/modules/global-classes/global-classes-repository.php
<?php
namespace Elementor\Modules\GlobalClasses;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Modules\DesignSystemSync\Classes\Global_Classes_Sync_Map;
use Elementor\Modules\GlobalClasses\Concerns\Has_Kit_Dependency;
use Elementor\Modules\GlobalClasses\Concerns\Has_Preview_Context;
use Elementor\Modules\GlobalClasses\Utils\Global_Class_Data_Normalizer;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Global_Classes_Repository {
use Has_Kit_Dependency;
use Has_Preview_Context;
const META_KEY_FRONTEND = '_elementor_global_classes';
const META_KEY_PREVIEW = '_elementor_global_classes_preview';
const CONTEXT_FRONTEND = 'frontend';
const CONTEXT_PREVIEW = 'preview';
const READ_BATCH_SIZE = 100;
const PERSIST_BATCH_SIZE = 100;
protected array $context_keys = [
'event' => [
'frontend' => self::CONTEXT_FRONTEND,
'preview' => self::CONTEXT_PREVIEW,
],
'meta_key' => [
'frontend' => self::META_KEY_FRONTEND,
'preview' => self::META_KEY_PREVIEW,
],
];
private ?Global_Classes $cache = null;
public function __construct( ?Kit $kit = null ) {
if ( null !== $kit ) {
$this->set_kit( $kit );
}
}
public static function make( ?Kit $kit = null ): Global_Classes_Repository {
return new self( $kit );
}
protected function on_preview_change(): void {
$this->cache = null;
}
/**
* This method may be too heavy to use
* Be mindful as this call would cause the server to freeze for as much time as needed until it fetches
* all global classes
*/
public function all( bool $force = false ): Global_Classes {
if ( ! $force && null !== $this->cache ) {
return $this->cache;
}
$this->cache = $this->all_from_posts();
return $this->cache;
}
public function all_labels(): array {
return $this->labels()->get_ordered_labels();
}
public function get_order(): array {
return Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() )->get_order();
}
public function update_order_and_labels( array $order, array $new_labels ): void {
Global_Classes_Order::make( $this->get_kit() )
->set_preview( $this->is_preview() )
->set_order( $order );
$labels = $this->labels();
$existing_labels = $labels->get_labels();
foreach ( $new_labels as $id => $label ) {
$existing_labels[ $id ] = $label;
}
$labels->set_labels( $existing_labels );
if ( ! $this->is_preview() ) {
Global_Classes_Order::make( $this->get_kit() )
->set_preview( true )
->set_order( $order );
$this->clear_preview_labels_for_ids( array_keys( $new_labels ) );
}
$this->cache = null;
}
private function labels(): Global_Classes_Labels {
return Global_Classes_Labels::make( $this->get_kit() )->set_preview( $this->is_preview() );
}
public function get( string $class_id ): ?array {
$post = Global_Class_Post::find_by_class_id( $class_id, $this->is_preview() );
return $post ? $post->to_array() : null;
}
public function get_by_ids( array $class_ids ): array {
if ( empty( $class_ids ) ) {
return [];
}
$post_ids = Global_Classes_Post_IDs::make( $this->get_kit() )->get_post_ids( $class_ids );
$items = [];
foreach ( $post_ids as $class_id => $post_id ) {
$post = Global_Class_Post::from_post_id( $post_id, $this->is_preview() );
if ( $post ) {
$items[ $class_id ] = $post->to_array();
}
}
return $items;
}
public function apply_changes( array $touched_items, array $changes, array $order ): void {
$labels = $this->labels();
$before = $labels->get_labels();
$is_preview = $this->is_preview();
$to_delete = $changes['deleted'] ?? [];
$to_create = $changes['added'] ?? [];
$to_update = $changes['modified'] ?? [];
$order_changed = isset( $changes['order'] ) && $changes['order'];
$final_label_map = [];
foreach ( $order as $id ) {
if ( isset( $touched_items[ $id ] ) ) {
$final_label_map[ $id ] = $touched_items[ $id ]['label'];
} elseif ( isset( $before[ $id ] ) ) {
$final_label_map[ $id ] = $before[ $id ];
}
}
$affected_post_ids = $this->get_posts_affected_by_deletion( $to_delete );
$this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $touched_items, $is_preview );
$classes_order = Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() );
$classes_order->set_order( $order );
$labels->set_labels( $final_label_map );
if ( ! $is_preview ) {
Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, $to_delete );
Global_Classes_Order::make( $this->get_kit() )
->set_preview( true )
->set_order( $order );
$this->bulk_clear_preview_meta( array_values( $to_update ) );
$this->clear_preview_labels_for_ids( array_merge(
array_values( $to_create ),
array_values( $to_update ),
array_values( $to_delete )
) );
}
$this->cache = null;
$this->flush_runtime_cache();
do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), [
'added' => $to_create,
'deleted' => $to_delete,
'modified' => $to_update,
'order' => $order_changed,
'affected_post_ids' => $affected_post_ids,
] );
if ( ! empty( $to_delete ) && ! $is_preview ) {
do_action(
'elementor/global_classes/cleanup',
$to_delete,
$affected_post_ids
);
}
}
public function each_item( callable $cb, bool $skip_migration = false, int $batch_size = self::READ_BATCH_SIZE ): void {
$order = Global_Classes_Order::make( $this->get_kit() )->get_order();
if ( empty( $order ) ) {
return;
}
foreach ( array_chunk( $order, $batch_size ) as $chunk ) {
foreach ( $this->iterate_class_posts_for_ids( $chunk ) as $class_post ) {
$cb( $class_post->to_array( $skip_migration ) );
}
}
}
public function put( array $items, array $order ) {
$current_ids = Global_Classes_Order::make( $this->get_kit() )
->set_preview( $this->is_preview() )
->get_order();
$new_ids = array_keys( $items );
$current_order_string = implode( ';', $current_ids );
$deleted_class_ids = array_values( array_diff( $current_ids, $new_ids ) );
$changes = [
'added' => array_values( array_diff( $new_ids, $current_ids ) ),
'deleted' => $deleted_class_ids,
'modified' => array_values( array_intersect( $new_ids, $current_ids ) ),
'order' => implode( ';', $order ) !== $current_order_string,
];
/**
* We collect all affected ids before the put_to_posts execution
* as the update mechanism would handle the Global_Classes_Relations as it iterates over the update batches
* So once we get to the cleanup phase - we would no longer have the relevant relations
*
* On top of that - by collecting all affected posts in advance, means we would iterate over each document's elements only once
* (as the alternative would be to trigger the cleanup per removed class, but that means we may end up iterating over the same document N times, if all N styles are used in it)
*/
$affected_post_ids = $this->get_posts_affected_by_deletion( $deleted_class_ids );
$this->put_to_posts( $items, $order, $current_ids );
$this->cache = null;
$changes['affected_post_ids'] = $affected_post_ids;
do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), $changes );
if ( ! empty( $deleted_class_ids ) && ! $this->is_preview() ) {
do_action(
'elementor/global_classes/cleanup',
$deleted_class_ids,
$affected_post_ids
);
}
}
private function get_posts_affected_by_deletion( array $deleted_class_ids ): array {
if ( empty( $deleted_class_ids ) ) {
return [];
}
$relations = new Global_Classes_Relations();
$post_ids = [];
foreach ( $deleted_class_ids as $class_id ) {
$post_ids[] = $relations->get_posts_by_style( $class_id );
}
return array_values( array_unique( array_merge( ...$post_ids ) ) );
}
private function all_from_posts(): Global_Classes {
$order = Global_Classes_Order::make( $this->get_kit() )
->set_preview( $this->is_preview() )
->get_order();
if ( empty( $order ) ) {
return Global_Classes::make( [], [] );
}
$items = [];
foreach ( $this->iterate_class_posts_for_ids( $order ) as $class_post ) {
$class_data = $class_post->to_array();
$items[ $class_data['id'] ] = $class_data;
}
$order = Global_Classes_Parser::sanitize_order( $items, $order );
return Global_Classes::make( $items, $order );
}
private function put_to_posts( array $items, array $order, array $current_ids ): void {
$is_preview = $this->is_preview();
$new_ids = array_keys( $items );
$to_delete = array_diff( $current_ids, $new_ids );
$to_create = array_diff( $new_ids, $current_ids );
$to_update = array_intersect( $new_ids, $current_ids );
$this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $items, $is_preview );
$classes_order = Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() );
$classes_order->set_order( $order );
$label_map = [];
foreach ( $order as $id ) {
if ( isset( $items[ $id ]['label'] ) ) {
$label_map[ $id ] = $items[ $id ]['label'];
}
}
$this->labels()->set_labels( $label_map );
if ( ! $is_preview ) {
$touched_ids = array_merge( array_values( $to_create ), array_values( $to_update ) );
$touched_items = array_intersect_key(
$items,
array_flip( $touched_ids )
);
Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, array_values( $to_delete ) );
$this->bulk_clear_preview_meta( array_values( $to_update ) );
$this->clear_preview_labels_for_ids( array_merge(
array_values( $to_create ),
array_values( $to_update ),
array_values( $to_delete )
) );
}
}
private function iterate_class_posts_for_ids( array $class_ids ): \Generator {
foreach ( array_chunk( $class_ids, self::READ_BATCH_SIZE ) as $chunk ) {
$posts = get_posts( [
'post_type' => Global_Class_Post_Type::CPT,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Global_Class_Post::META_KEY_ID,
'value' => $chunk,
'compare' => 'IN',
],
],
] );
foreach ( $posts as $post ) {
yield Global_Class_Post::from_post( $post, $this->is_preview() );
clean_post_cache( $post->ID );
}
unset( $posts );
$this->flush_runtime_cache();
}
}
private function persist_class_batch_mutations(
array $to_delete,
array $to_create,
array $to_update,
array $items_by_id,
bool $is_preview
): void {
$relations = new Global_Classes_Relations();
$post_ids_map = Global_Classes_Post_IDs::make( $this->get_kit() );
$ids_to_resolve = array_values( array_merge(
array_values( $to_delete ),
array_values( $to_update ),
array_values( $to_create )
) );
$post_ids = $post_ids_map->get_post_ids( $ids_to_resolve );
$this->each_class_id_batch(
array_values( $to_delete ),
function ( string $class_id ) use ( $is_preview, $relations, $post_ids ) {
$post = isset( $post_ids[ $class_id ] )
? Global_Class_Post::from_post_id( $post_ids[ $class_id ], false )
: null;
if ( ! $post ) {
return;
}
if ( $is_preview ) {
$post->set_preview( true );
$post->update_data( [] );
clean_post_cache( $post->get_post_id() );
} else {
$relations->clear_class_relations( $class_id );
$post->delete();
}
}
);
$this->each_class_id_batch( $to_create, function ( string $class_id ) use ( $items_by_id, $is_preview, $post_ids, $post_ids_map ) {
if ( ! isset( $items_by_id[ $class_id ] ) ) {
return;
}
$item = $items_by_id[ $class_id ];
$data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
$kit = $this->get_kit();
$existing_post_id = $post_ids[ $class_id ] ?? null;
$existing_post = $existing_post_id ? Global_Class_Post::from_post_id( $existing_post_id, $is_preview ) : null;
if ( $existing_post ) {
$existing_post->update_data( $data );
if ( ! $is_preview ) {
$existing_post->update_label( $item['label'] );
}
clean_post_cache( $existing_post->get_post_id() );
return;
}
$created = Global_Class_Post::create( $class_id, $item['label'], $data, $kit );
if ( $created ) {
$post_ids_map->set( $class_id, $created->get_post_id() );
clean_post_cache( $created->get_post_id() );
}
} );
$this->each_class_id_batch(
$to_update,
function ( string $class_id ) use ( $items_by_id, $is_preview, $post_ids ) {
if ( ! isset( $items_by_id[ $class_id ] ) || ! isset( $post_ids[ $class_id ] ) ) {
return;
}
$item = $items_by_id[ $class_id ];
$post = Global_Class_Post::from_post_id( $post_ids[ $class_id ], $is_preview );
if ( ! $post ) {
return;
}
$data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
$post->update_data( $data );
if ( ! $is_preview ) {
$post->update_label( $item['label'] );
}
clean_post_cache( $post->get_post_id() );
}
);
}
public function delete_all(): void {
$order = $this->get_order();
$this->each_class_id_batch( $order, function ( string $class_id ) {
$post = Global_Class_Post::find_by_class_id( $class_id );
if ( $post ) {
$post->delete();
}
} );
Global_Classes_Order::make( $this->get_kit() )->set_order( [] );
$this->labels()->set_labels( [] );
}
private function clear_preview_labels_for_ids( array $class_ids ): void {
if ( empty( $class_ids ) ) {
return;
}
$preview_labels = Global_Classes_Labels::make( $this->get_kit() )->set_preview( true );
$labels_map = $preview_labels->get_labels();
if ( empty( $labels_map ) ) {
return;
}
$ids_to_remove = array_intersect( array_unique( $class_ids ), array_keys( $labels_map ) );
if ( empty( $ids_to_remove ) ) {
return;
}
foreach ( $ids_to_remove as $id ) {
unset( $labels_map[ $id ] );
}
$preview_labels->set_labels( $labels_map );
}
private function bulk_clear_preview_meta( array $class_ids ): void {
if ( empty( $class_ids ) ) {
return;
}
$post_ids_map = Global_Classes_Post_IDs::make( $this->get_kit() );
foreach ( array_chunk( $class_ids, self::PERSIST_BATCH_SIZE ) as $chunk ) {
$post_ids = $post_ids_map->get_post_ids( $chunk );
foreach ( $post_ids as $post_id ) {
delete_post_meta( $post_id, Global_Class_Post::META_KEY_DATA_PREVIEW );
clean_post_cache( $post_id );
}
$this->flush_runtime_cache();
}
}
private function each_class_id_batch( $class_ids, callable $callback, int $batch_size = self::PERSIST_BATCH_SIZE ): void {
$class_ids = is_array( $class_ids ) ? $class_ids : iterator_to_array( $class_ids, false );
foreach ( array_chunk( array_values( $class_ids ), $batch_size ) as $batch ) {
foreach ( $batch as $class_id ) {
$callback( $class_id );
}
$this->flush_runtime_cache();
}
}
private function flush_runtime_cache(): void {
if ( function_exists( 'wp_cache_flush_runtime' ) ) {
wp_cache_flush_runtime();
}
}
}