[ Index ]

PHP Cross Reference of GlotPress

title

Body

[close]

/gp-includes/things/ -> original.php (source)

   1  <?php
   2  /**
   3   * Things: GP_Original class
   4   *
   5   * @package GlotPress
   6   * @subpackage Things
   7   * @since 1.0.0
   8   */
   9  
  10  /**
  11   * Core class used to implement the originals.
  12   *
  13   * @since 1.0.0
  14   */
  15  class GP_Original extends GP_Thing {
  16  
  17      var $table_basename           = 'gp_originals';
  18      var $field_names              = array( 'id', 'project_id', 'context', 'singular', 'plural', 'references', 'comment', 'status', 'priority', 'date_added' );
  19      var $int_fields               = array( 'id', 'project_id', 'priority' );
  20      var $non_updatable_attributes = array( 'id', 'path' );
  21  
  22      public $id;
  23      public $project_id;
  24      public $context;
  25      public $singular;
  26      public $plural;
  27      public $references;
  28      public $comment;
  29      public $status;
  30      public $priority;
  31      public $date_added;
  32  
  33      static $priorities = array(
  34          '-2' => 'hidden',
  35          '-1' => 'low',
  36          '0'  => 'normal',
  37          '1'  => 'high',
  38      );
  39  
  40      static $count_cache_group = 'active_originals_count_by_project_id';
  41  
  42      /**
  43       * Sets restriction rules for fields.
  44       *
  45       * @since 1.0.0
  46       *
  47       * @param GP_Validation_Rules $rules The validation rules instance.
  48       */
  49  	public function restrict_fields( $rules ) {
  50          $rules->singular_should_not_be( 'empty_string' );
  51          $rules->status_should_not_be( 'empty' );
  52          $rules->project_id_should_be( 'positive_int' );
  53          $rules->priority_should_be( 'int' );
  54          $rules->priority_should_be( 'between', -2, 1 );
  55      }
  56  
  57      /**
  58       * Normalizes an array with key-value pairs representing
  59       * a GP_Original object.
  60       *
  61       * @since 1.0.0
  62       *
  63       * @param array $args Arguments for a GP_Original object.
  64       * @return array Normalized arguments for a GP_Original object.
  65       */
  66  	public function normalize_fields( $args ) {
  67          foreach ( array( 'plural', 'context', 'references', 'comment' ) as $field ) {
  68              if ( isset( $args['parent_project_id'] ) ) {
  69                  $args[ $field ] = $this->force_false_to_null( $args[ $field ] );
  70              }
  71          }
  72  
  73          if ( isset( $args['priority'] ) && ! is_numeric( $args['priority'] ) ) {
  74              $args['priority'] = $this->priority_by_name( $args['priority'] );
  75              if ( is_null( $args['priority'] ) ) {
  76                  unset( $args['priority'] );
  77              }
  78          }
  79  
  80          $args = parent::normalize_fields( $args );
  81  
  82          return $args;
  83      }
  84  
  85  	public function by_project_id( $project_id ) {
  86          return $this->many( "SELECT * FROM $this->table WHERE project_id= %d AND status = '+active'", $project_id );
  87      }
  88  
  89      /**
  90       * Retrieves the number of originals for a project.
  91       *
  92       * @since 1.0.0
  93       * @since 2.1.0 Added the `$type` parameter.
  94       *
  95       * @param int    $project_id The ID of a project.
  96       * @param string $type       The return type. 'total' for public and hidden counts, 'hidden'
  97       *                           for hidden count, 'public' for public count, 'all' for all three
  98       *                           values. Default 'total'.
  99       * @return object|int Object when `$type` is 'all', non-negative integer in all other cases.
 100       */
 101  	public function count_by_project_id( $project_id, $type = 'total' ) {
 102          global $wpdb;
 103  
 104          // If an unknown type has been passed in, just return a 0 result immediately instead of running the SQL code.
 105          if ( ! in_array( $type, array( 'total', 'hidden', 'public', 'all' ), true ) ) {
 106              return 0;
 107          }
 108  
 109          // Get the cache and use it if possible.
 110          $cached = wp_cache_get( $project_id, self::$count_cache_group );
 111          if ( false !== $cached && is_object( $cached ) ) { // Since 2.1.0 stdClass.
 112              if ( 'all' === $type ) {
 113                  return $cached;
 114              } elseif ( isset( $cached->$type ) ) {
 115                  return $cached->$type;
 116              }
 117  
 118              // If we've fallen through for some reason, make sure to return an integer 0.
 119              return 0;
 120          }
 121  
 122          // No cache values found so let's query the database for the results.
 123          $counts = $wpdb->get_row(
 124              $wpdb->prepare(
 125                  "SELECT
 126                      COUNT(*) AS total,
 127                      COUNT( CASE WHEN priority = '-2' THEN priority END ) AS `hidden`,
 128                      COUNT( CASE WHEN priority <> '-2' THEN priority END ) AS `public`
 129                  FROM {$wpdb->gp_originals}
 130                  WHERE
 131                      project_id = %d AND status = '+active'",
 132                  $project_id
 133              ),
 134              ARRAY_A
 135          );
 136  
 137          // Make sure $wpdb->get_row() returned an array, if not set all results to 0.
 138          if ( ! is_array( $counts ) ) {
 139              $counts = array(
 140                  'total'  => 0,
 141                  'hidden' => 0,
 142                  'public' => 0,
 143              );
 144          }
 145  
 146          // Make sure counts are integers.
 147          $counts = (object) array_map( 'intval', $counts );
 148  
 149          wp_cache_set( $project_id, $counts, self::$count_cache_group );
 150  
 151          if ( 'all' === $type ) {
 152              return $counts;
 153          } elseif ( isset( $counts->$type ) ) {
 154              return $counts->$type;
 155          }
 156  
 157          // If we've fallen through for some reason, make sure to return an integer 0.
 158          return 0;
 159      }
 160  
 161  
 162  	public function by_project_id_and_entry( $project_id, $entry, $status = null ) {
 163          global $wpdb;
 164  
 165          $entry->plural  = isset( $entry->plural ) ? $entry->plural : null;
 166          $entry->context = isset( $entry->context ) ? $entry->context : null;
 167  
 168          $where = array();
 169          // now each condition has to contain a %s not to break the sequence
 170          $where[] = is_null( $entry->context ) ? '(context IS NULL OR %s IS NULL)' : 'context = BINARY %s';
 171          $where[] = 'singular = BINARY %s';
 172          $where[] = is_null( $entry->plural ) ? '(plural IS NULL OR %s IS NULL)' : 'plural = BINARY %s';
 173          $where[] = 'project_id = %d';
 174  
 175          if ( ! is_null( $status ) ) {
 176              $where[] = $wpdb->prepare( 'status = %s', $status );
 177          }
 178  
 179          $where = implode( ' AND ', $where );
 180  
 181          return $this->one( "SELECT * FROM $this->table WHERE $where", $entry->context, $entry->singular, $entry->plural, $project_id );
 182      }
 183  
 184  	public function import_for_project( $project, $translations ) {
 185          global $wpdb;
 186  
 187          $originals_added = $originals_existing = $originals_obsoleted = $originals_fuzzied = $originals_error = 0;
 188  
 189          $all_originals_for_project = $this->many_no_map( "SELECT * FROM $this->table WHERE project_id= %d", $project->id );
 190          $originals_by_key          = array();
 191          foreach ( $all_originals_for_project as $original ) {
 192              $entry = new Translation_Entry(
 193                  array(
 194                      'singular' => $original->singular,
 195                      'plural'   => $original->plural,
 196                      'context'  => $original->context,
 197                  )
 198              );
 199  
 200              $originals_by_key[ $entry->key() ] = $original;
 201          }
 202  
 203          $obsolete_originals = array_filter(
 204              $originals_by_key,
 205              function( $entry ) {
 206                  return ( '-obsolete' == $entry->status );
 207              }
 208          );
 209  
 210          $possibly_added = $possibly_dropped = array();
 211  
 212          foreach ( $translations->entries as $key => $entry ) {
 213              $wpdb->queries = array();
 214  
 215              // Context needs to match VARCHAR(255) in the database schema.
 216              if ( mb_strlen( $entry->context ) > 255 ) {
 217                  $entry->context                         = mb_substr( $entry->context, 0, 255 );
 218                  $translations->entries[ $entry->key() ] = $entry;
 219              }
 220  
 221              $data = array(
 222                  'project_id' => $project->id,
 223                  'context'    => $entry->context,
 224                  'singular'   => $entry->singular,
 225                  'plural'     => $entry->plural,
 226                  'comment'    => $entry->extracted_comments,
 227                  'references' => implode( ' ', $entry->references ),
 228                  'status'     => '+active',
 229              );
 230  
 231              /**
 232               * Filter the data of an original being imported or updated.
 233               *
 234               * This filter is called twice per each entry. First time during determining if the original
 235               * already exists. The second time it is called before a new original is added or a close
 236               * old match is set fuzzy with this new data.
 237               *
 238               * @since 1.0.0
 239               *
 240               * @param array $data {
 241               *     An array that describes a single entry being imported or updated.
 242               *
 243               *     @type string $project_id Project id to import into.
 244               *     @type string $context    Context information.
 245               *     @type string $singular   Translation string of the singular form.
 246               *     @type string $plural     Translation string of the plural form.
 247               *     @type string $comment    Comment for translators.
 248               *     @type string $references Referenced in code. A single reference is represented by a file
 249               *                              path followed by a colon and a line number. Multiple references
 250               *                              are separated by spaces.
 251               *     @type string $status     Status of the imported original.
 252               * }
 253               */
 254              $data = apply_filters( 'gp_import_original_array', $data );
 255  
 256              // Original exists, let's update it.
 257              if ( isset( $originals_by_key[ $entry->key() ] ) ) {
 258                  $original = $originals_by_key[ $entry->key() ];
 259                  // But only if it's different, like a changed 'references', 'comment', or 'status' field.
 260                  if ( GP::$original->is_different_from( $data, $original ) ) {
 261                      $this->update( $data, array( 'id' => $original->id ) );
 262                      $originals_existing++;
 263                  }
 264              } else {
 265                  // We can't find this in our originals. Let's keep it for later.
 266                  $possibly_added[] = $entry;
 267              }
 268          }
 269  
 270          // Mark missing strings as possible removals.
 271          foreach ( $originals_by_key as $key => $value ) {
 272              if ( '-obsolete' != $value->status && is_array( $translations->entries ) && ! array_key_exists( $key, $translations->entries ) ) {
 273                  $possibly_dropped[ $key ] = $value;
 274              }
 275          }
 276          $comparison_array = array_unique( array_merge( array_keys( $possibly_dropped ), array_keys( $obsolete_originals ) ) );
 277  
 278          $prev_suspend_cache = wp_suspend_cache_invalidation( true );
 279  
 280          foreach ( $possibly_added as $entry ) {
 281              $data = array(
 282                  'project_id' => $project->id,
 283                  'context'    => $entry->context,
 284                  'singular'   => $entry->singular,
 285                  'plural'     => $entry->plural,
 286                  'comment'    => $entry->extracted_comments,
 287                  'references' => implode( ' ', $entry->references ),
 288                  'status'     => '+active',
 289              );
 290  
 291              /** This filter is documented in gp-includes/things/original.php */
 292              $data = apply_filters( 'gp_import_original_array', $data );
 293  
 294              // Search for match in the dropped strings and existing obsolete strings.
 295              $close_original = $this->closest_original( $entry->key(), $comparison_array );
 296  
 297              // We found a match - probably a slightly changed string.
 298              if ( $close_original ) {
 299                  $original = $originals_by_key[ $close_original ];
 300  
 301                  /**
 302                   * Filters whether to set existing translations to fuzzy.
 303                   *
 304                   * This filter is called when a new  string closely match an existing possibly dropped string.
 305                   *
 306                   * @since 2.3.0
 307                   *
 308                   * @param bool   $do_fuzzy Whether to set existing translations to fuzzy. Default true.
 309                   * @param object $data     The new original data.
 310                   * @param object $original The previous original being replaced.
 311                   */
 312                  $do_fuzzy = apply_filters( 'gp_set_translations_for_original_to_fuzzy', true, (object) $data, $original );
 313  
 314                  // We'll update the old original...
 315                  $this->update( $data, array( 'id' => $original->id ) );
 316  
 317                  // and set existing translations to fuzzy.
 318                  if ( $do_fuzzy ) {
 319                      $this->set_translations_for_original_to_fuzzy( $original->id );
 320                      $originals_fuzzied++;
 321                  } else {
 322                      $originals_existing++;
 323                  }
 324  
 325                  // No need to obsolete it now.
 326                  unset( $possibly_dropped[ $close_original ] );
 327  
 328                  continue;
 329              } else { // Completely new string
 330                  $created = GP::$original->create( $data );
 331  
 332                  if ( ! $created ) {
 333                      $originals_error++;
 334                      continue;
 335                  }
 336  
 337                  $originals_added++;
 338              }
 339          }
 340  
 341          // Mark remaining possibly dropped strings as obsolete.
 342          foreach ( $possibly_dropped as $key => $value ) {
 343              $this->update( array( 'status' => '-obsolete' ), array( 'id' => $value->id ) );
 344              $originals_obsoleted++;
 345          }
 346  
 347          wp_suspend_cache_invalidation( $prev_suspend_cache );
 348  
 349          // Clear cache when the amount of strings are changed.
 350          if ( $originals_added > 0 || $originals_existing > 0 || $originals_fuzzied > 0 || $originals_obsoleted > 0 ) {
 351              wp_cache_delete( $project->id, self::$count_cache_group );
 352              gp_clean_translation_sets_cache( $project->id );
 353          }
 354  
 355          /**
 356           * Fires after originals have been imported.
 357           *
 358           * @since 1.0.0
 359           *
 360           * @param string $project_id          Project ID the import was made to.
 361           * @param int    $originals_added     Number or total originals added.
 362           * @param int    $originals_existing  Number of existing originals updated.
 363           * @param int    $originals_obsoleted Number of originals that were marked as obsolete.
 364           * @param int    $originals_fuzzied   Number of originals that were close matches of old ones and thus marked as fuzzy.
 365           * @param int    $originals_error     Number of originals that were not imported due to an error.
 366           */
 367          do_action( 'gp_originals_imported', $project->id, $originals_added, $originals_existing, $originals_obsoleted, $originals_fuzzied, $originals_error );
 368  
 369          return array( $originals_added, $originals_existing, $originals_fuzzied, $originals_obsoleted, $originals_error );
 370      }
 371  
 372  	public function set_translations_for_original_to_fuzzy( $original_id ) {
 373          $translations = GP::$translation->find_many( "original_id = '$original_id' AND status = 'current'" );
 374          foreach ( $translations as $translation ) {
 375              $translation->set_status( 'fuzzy' );
 376          }
 377      }
 378  
 379  	public function is_different_from( $data, $original = null ) {
 380          if ( ! $original ) {
 381              $original = $this;
 382          }
 383  
 384          foreach ( $data as $field => $value ) {
 385              if ( $original->$field != $value ) {
 386                  return true;
 387              }
 388          }
 389          return false;
 390      }
 391  
 392  	public function priority_by_name( $name ) {
 393          $by_name = array_flip( self::$priorities );
 394          return isset( $by_name[ $name ] ) ? $by_name[ $name ] : null;
 395      }
 396  
 397  	public function closest_original( $input, $other_strings ) {
 398          if ( empty( $other_strings ) ) {
 399              return null;
 400          }
 401  
 402          $input_length       = mb_strlen( $input );
 403          $closest_similarity = 0;
 404  
 405          foreach ( $other_strings as $compared_string ) {
 406              $compared_string_length = mb_strlen( $compared_string );
 407  
 408              /**
 409               * Filter the maximum length difference allowed when comparing originals for a close match when importing.
 410               *
 411               * @since 1.0.0
 412               *
 413               * @param float $max_length_diff The times compared string length can differ from the input string.
 414               */
 415              $max_length_diff = apply_filters( 'gp_original_import_max_length_diff', 0.5 );
 416  
 417              if ( abs( ( $input_length - $compared_string_length ) / $input_length ) > $max_length_diff ) {
 418                  continue;
 419              }
 420  
 421              $similarity = gp_string_similarity( $input, $compared_string );
 422  
 423              if ( $similarity > $closest_similarity ) {
 424                  $closest            = $compared_string;
 425                  $closest_similarity = $similarity;
 426              }
 427          }
 428  
 429          if ( ! isset( $closest ) ) {
 430              return null;
 431          }
 432  
 433          /**
 434           * Filter the minimum allowed similarity to be considered as a close match.
 435           *
 436           * @since 1.0.0
 437           *
 438           * @param float $similarity Minimum allowed similarity.
 439           */
 440          $min_score    = apply_filters( 'gp_original_import_min_similarity_diff', 0.8 );
 441          $close_enough = ( $closest_similarity > $min_score );
 442  
 443          /**
 444           * Fires after determining string similarity.
 445           *
 446           * @since 1.0.0
 447           *
 448           * @param string $input              The original string to match against.
 449           * @param string $closest            Closest matching string.
 450           * @param float  $closest_similarity The similarity between strings that was calculated.
 451           * @param bool   $close_enough       Whether the closest was be determined as close enough match.
 452           */
 453          do_action( 'gp_post_string_similarity_test', $input, $closest, $closest_similarity, $close_enough );
 454  
 455          if ( $close_enough ) {
 456              return $closest;
 457          } else {
 458              return null;
 459          }
 460      }
 461  
 462  	public function get_matching_originals_in_other_projects() {
 463          $where   = array();
 464          $where[] = 'singular = BINARY %s';
 465          $where[] = is_null( $this->plural ) ? '(plural IS NULL OR %s IS NULL)' : 'plural = BINARY %s';
 466          $where[] = is_null( $this->context ) ? '(context IS NULL OR %s IS NULL)' : 'context = BINARY %s';
 467          $where[] = 'project_id != %d';
 468          $where[] = "status = '+active'";
 469          $where   = implode( ' AND ', $where );
 470  
 471          return GP::$original->many( "SELECT * FROM $this->table WHERE $where", $this->singular, $this->plural, $this->context, $this->project_id );
 472      }
 473  
 474      // Triggers
 475  
 476      /**
 477       * Executes after creating an original.
 478       *
 479       * @since 1.0.0
 480       *
 481       * @return bool
 482       */
 483  	public function after_create() {
 484          /**
 485           * Fires after a new original is created.
 486           *
 487           * @since 1.0.0
 488           *
 489           * @param GP_original $original The original that was created.
 490           */
 491          do_action( 'gp_original_created', $this );
 492  
 493          return true;
 494      }
 495  
 496      /**
 497       * Executes after saving an original.
 498       *
 499       * @since 2.0.0
 500       * @since 3.0.0 Added the `$original_before` parameter.
 501       *
 502       * @param GP_Original $original_before Original before the update.
 503       * @return bool
 504       */
 505  	public function after_save( $original_before ) {
 506          /**
 507           * Fires after an original is saved.
 508           *
 509           * @since 2.0.0
 510           * @since 3.0.0 Added the `$original_before` parameter.
 511           *
 512           * @param GP_Original $original        Original following the update.
 513           * @param GP_Original $original_before Original before the update.
 514           */
 515          do_action( 'gp_original_saved', $this, $original_before );
 516  
 517          return true;
 518      }
 519  
 520      /**
 521       * Executes after deleting an original.
 522       *
 523       * @since 2.0.0
 524       *
 525       * @return bool
 526       */
 527  	public function after_delete() {
 528          /**
 529           * Fires after an original is deleted.
 530           *
 531           * @since 2.0.0
 532           *
 533           * @param GP_original $original The original that was deleted.
 534           */
 535          do_action( 'gp_original_deleted', $this );
 536  
 537          return true;
 538      }
 539  }
 540  GP::$original = new GP_Original();


Generated: Sun Jul 12 01:02:16 2020 Cross-referenced by PHPXref 0.7.1