class-wpml-hierarchy-sync.php 9.9 KB
<?php

abstract class WPML_Hierarchy_Sync extends WPML_WPDB_User {

	const CACHE_GROUP = __CLASS__;

	protected $original_elements_table_alias            = 'org';
	protected $translated_elements_table_alias          = 'tra';
	protected $original_elements_language_table_alias   = 'iclo';
	protected $translated_elements_language_table_alias = 'iclt';
	protected $correct_parent_table_alias               = 'corr';
	protected $correct_parent_language_table_alias      = 'iclc';
	protected $original_parent_table_alias              = 'parents';
	protected $original_parent_language_table_alias     = 'parent_lang';
	protected $element_id_column;
	protected $parent_element_id_column;
	protected $parent_id_column;
	protected $element_type_column;
	protected $element_type_prefix;
	protected $elements_table;
	protected $lang_info_table;

	/**
	 * @param wpdb $wpdb
	 */
	public function __construct( &$wpdb ) {
		parent::__construct( $wpdb );
		$this->lang_info_table = $wpdb->prefix . 'icl_translations';
		add_action( 'clean_post_cache', [ $this, 'clean_cache' ] );
		add_action( 'set_object_terms', [ $this, 'clean_cache' ] );
		add_action( 'wpml_sync_term_hierarchy_done', [ $this, 'clean_cache' ] );
	}

	public function clean_cache() {
		WPML_Non_Persistent_Cache::flush_group( self::CACHE_GROUP );
	}

	public function get_unsynced_elements( $element_types, $ref_lang_code = false ) {
		$element_types = (array) $element_types;
		$results       = array();
		if ( $element_types ) {
			$key     = md5( (string) wp_json_encode( array( $element_types, $ref_lang_code ) ) );
			$found   = false;
			$results = WPML_Non_Persistent_Cache::get( $key, self::CACHE_GROUP, $found );
			if ( ! $found ) {
				$results_sql_parts = array();

				$results_sql_parts['source_element_table']           = $this->get_source_element_table();
				$results_sql_parts['source_element_join']            = $this->get_source_element_join();
				$results_sql_parts['join_translation_language_data'] = $this->get_join_translation_language_data( $ref_lang_code );
				$results_sql_parts['translated_element_join']        = $this->get_translated_element_join();
				$results_sql_parts['original_parent_join']           = $this->get_original_parent_join();
				$results_sql_parts['original_parent_language_join']  = $this->get_original_parent_language_join();
				$results_sql_parts['correct_parent_language_join']   = $this->get_correct_parent_language_join();
				$results_sql_parts['correct_parent_element_join']    = $this->get_correct_parent_element_join();
				$results_sql_parts['where_statement']                = $this->get_where_statement(
					$element_types,
					$ref_lang_code
				);

				$results_sql = $this->get_select_statement();

				$results_sql .= ' FROM ';
				$results_sql .= implode( ' ', $results_sql_parts );

				$results = $this->wpdb->get_results( $results_sql );

				WPML_Non_Persistent_Cache::set( $key, $results, self::CACHE_GROUP );
			}
		}

		return $results;
	}

	/**
	 * @param string|array $element_types
	 * @param bool         $ref_lang_code
	 */
	public function sync_element_hierarchy( $element_types, $ref_lang_code = false ) {
		$hierarchical_element_types = wpml_collect( $element_types )->filter( [ $this, 'is_hierarchical' ] );

		if ( $hierarchical_element_types->isEmpty() ) {
			return;
		}

		$unsynced = $this->get_unsynced_elements( $hierarchical_element_types->toArray(), $ref_lang_code );

		foreach ( $unsynced as $row ) {
			$this->update_hierarchy_for_element( $row );
		}
	}

	/**
	 * @param string $element_type
	 *
	 * @return mixed
	 */
	abstract public function is_hierarchical( $element_type );

	private function update_hierarchy_for_element( $row ) {
		$update = $this->validate_parent_synchronization( $row );

		if ( $update ) {
			$target_element_id = $row->translated_id;
			$new_parent        = (int) $row->correct_parent;
			$this->wpdb->update( $this->elements_table, array( $this->parent_id_column => $new_parent ), array( $this->element_id_column => $target_element_id ) );
		}
	}

	private function validate_parent_synchronization( $row ) {
		$is_valid     = false;
		$is_for_posts = ( $this->elements_table === $this->wpdb->posts );
		if ( ! $is_for_posts ) {
			$is_valid = true;
		}

		if ( $row && $is_for_posts ) {
			global $sitepress;

			$target_element_id = $row->translated_id;
			$target_post       = get_post( $target_element_id );
			if ( $target_post ) {
				$parent_must_empty       = false;
				$post_type               = $target_post->post_type;
				$element_type            = 'post_' . $post_type;
				$target_element_language = $sitepress->get_element_language_details( $target_element_id, $element_type );
				$original_element_id     = $sitepress->get_original_element_id( $target_element_id, $element_type );
				if ( $original_element_id ) {
					$parent_has_translation_in_target_language = false;

					$original_element        = get_post( $original_element_id );
					$original_post_parent_id = $original_element->post_parent;
					if ( $original_post_parent_id ) {
						$original_post_parent_trid         = $sitepress->get_element_trid( $original_post_parent_id, $element_type );
						$original_post_parent_translations = $sitepress->get_element_translations( $original_post_parent_trid, $element_type );
						foreach ( $original_post_parent_translations as $original_post_parent_translation ) {
							if ( $original_post_parent_translation->language_code == $target_element_language->language_code ) {
								$parent_has_translation_in_target_language = true;
								break;
							}
						}
					} else {
						$parent_must_empty = true;
					}
					/**
					 * Check if the parent of the original post has a translation in the language of the target post or if the parent must be set to 0
					 */
					$is_valid = $parent_has_translation_in_target_language || $parent_must_empty;
				}
			}
		}

		return $is_valid;
	}

	private function get_source_element_join() {

		return "JOIN {$this->lang_info_table} {$this->original_elements_language_table_alias}
					ON {$this->original_elements_table_alias}.{$this->element_id_column}
						= {$this->original_elements_language_table_alias}.element_id
	                    AND {$this->original_elements_language_table_alias}.element_type
	                        = CONCAT('{$this->element_type_prefix}', {$this->original_elements_table_alias}.{$this->element_type_column})";
	}

	private function get_translated_element_join() {

		return "JOIN {$this->elements_table} {$this->translated_elements_table_alias}
					ON {$this->translated_elements_table_alias}.{$this->element_id_column}
					= {$this->translated_elements_language_table_alias}.element_id ";
	}

	private function get_source_element_table() {

		return " {$this->elements_table} {$this->original_elements_table_alias} ";
	}

	private function get_join_translation_language_data( $ref_language_code ) {

		$res = " JOIN {$this->lang_info_table} {$this->translated_elements_language_table_alias}
	               ON {$this->translated_elements_language_table_alias}.trid
	                 = {$this->original_elements_language_table_alias}.trid ";
		if ( (bool) $ref_language_code === true ) {
			$res .= "AND {$this->translated_elements_language_table_alias}.language_code
						!= {$this->original_elements_language_table_alias}.language_code ";
		} else {
			$res .= " AND {$this->translated_elements_language_table_alias}.source_language_code
                         = {$this->original_elements_language_table_alias}.language_code ";
		}

		return $res;
	}

	private function get_select_statement() {

		return " SELECT {$this->translated_elements_table_alias}.{$this->element_id_column} AS translated_id
						 , IFNULL({$this->correct_parent_table_alias}.{$this->parent_element_id_column}, 0) AS correct_parent ";
	}

	private function get_original_parent_join() {

		return " LEFT JOIN {$this->elements_table} {$this->original_parent_table_alias}
	                ON {$this->original_parent_table_alias}.{$this->parent_element_id_column}
	                    = {$this->original_elements_table_alias}.{$this->parent_id_column} ";
	}

	private function get_original_parent_language_join() {

		return " LEFT JOIN {$this->lang_info_table} {$this->original_parent_language_table_alias}
	               ON {$this->original_parent_table_alias}.{$this->element_id_column}
	                = {$this->original_parent_language_table_alias}.element_id
	                 AND {$this->original_parent_language_table_alias}.element_type
	                     = CONCAT('{$this->element_type_prefix}', {$this->original_parent_table_alias}.{$this->element_type_column}) ";
	}

	private function get_correct_parent_language_join() {

		return " LEFT JOIN {$this->lang_info_table} {$this->correct_parent_language_table_alias}
	              ON {$this->correct_parent_language_table_alias}.language_code
	                    = {$this->translated_elements_language_table_alias}.language_code
	                AND {$this->original_parent_language_table_alias}.trid
	                    = {$this->correct_parent_language_table_alias}.trid ";
	}

	private function get_correct_parent_element_join() {

		return " LEFT JOIN {$this->elements_table} {$this->correct_parent_table_alias}
	              ON {$this->correct_parent_table_alias}.{$this->element_id_column}
	                = {$this->correct_parent_language_table_alias}.element_id ";
	}

	private function get_where_statement( $element_types, $ref_lang_code ) {

		$filter_originals_snippet = $ref_lang_code
			? $this->wpdb->prepare( " AND {$this->original_elements_language_table_alias}.language_code = %s ", $ref_lang_code )
			: " AND {$this->translated_elements_language_table_alias}.source_language_code IS NOT NULL ";

		return " WHERE {$this->original_elements_table_alias}.{$this->element_type_column}
					IN (" . wpml_prepare_in( $element_types ) . ")
                    AND IFNULL({$this->correct_parent_table_alias}.{$this->parent_element_id_column}, 0)
                        != {$this->translated_elements_table_alias}.{$this->parent_id_column} " . $filter_originals_snippet;
	}
}