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


Generated: Mon Nov 18 01:01:56 2019 Cross-referenced by PHPXref 0.7.1