Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 188 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
| SectionTranslationStore | |
0.00% |
0 / 188 |
|
0.00% |
0 / 14 |
992 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| insertTranslation | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| insertMultipleTranslations | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
| updateTranslation | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| updateTranslationStatusById | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| findTranslation | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
| findTranslationBySectionTitle | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
| createTranslationFromRow | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
| getStatusIndexByStatus | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| doFindTranslationsByUser | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
| findDraftSectionTranslationsByUser | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
| findPublishedSectionTranslationsByUser | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
42 | |||
| deleteTranslationById | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| translationToDBRow | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace ContentTranslation\Store; |
| 6 | |
| 7 | use ContentTranslation\DTO\DraftTranslationDTO; |
| 8 | use ContentTranslation\DTO\PublishedSectionTranslationDTO; |
| 9 | use ContentTranslation\DTO\PublishedTranslationDTO; |
| 10 | use ContentTranslation\Entity\SectionTranslation; |
| 11 | use InvalidArgumentException; |
| 12 | use Wikimedia\Rdbms\IConnectionProvider; |
| 13 | use Wikimedia\Rdbms\IDatabase; |
| 14 | use Wikimedia\Rdbms\IResultWrapper; |
| 15 | use Wikimedia\Rdbms\Platform\ISQLPlatform; |
| 16 | use Wikimedia\Rdbms\SelectQueryBuilder; |
| 17 | |
| 18 | class SectionTranslationStore { |
| 19 | public const TABLE_NAME = 'cx_section_translations'; |
| 20 | public const TRANSLATION_STATUS_DRAFT = 'draft'; |
| 21 | public const TRANSLATION_STATUS_PUBLISHED = 'published'; |
| 22 | public const TRANSLATION_STATUS_DELETED = 'deleted'; |
| 23 | |
| 24 | /** |
| 25 | * This constant contains the mappings of translation statuses to integers, |
| 26 | * that are used as values for "cxsx_translation_status" field inside the database. |
| 27 | * This constant should NOT be changed or reordered. |
| 28 | */ |
| 29 | public const TRANSLATION_STATUSES = [ |
| 30 | 0 => self::TRANSLATION_STATUS_DRAFT, |
| 31 | 1 => self::TRANSLATION_STATUS_PUBLISHED, |
| 32 | 2 => self::TRANSLATION_STATUS_DELETED, |
| 33 | ]; |
| 34 | |
| 35 | public function __construct( private readonly IConnectionProvider $connectionProvider ) { |
| 36 | } |
| 37 | |
| 38 | public function insertTranslation( SectionTranslation $translation ) { |
| 39 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 40 | $values = $this->translationToDBRow( $translation ); |
| 41 | // set start/last_updated timestamps to current timestamp |
| 42 | $values['cxsx_translation_start_timestamp'] = $dbw->timestamp(); |
| 43 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
| 44 | |
| 45 | $dbw->newInsertQueryBuilder() |
| 46 | ->insertInto( self::TABLE_NAME ) |
| 47 | ->row( $values ) |
| 48 | ->caller( __METHOD__ ) |
| 49 | ->execute(); |
| 50 | $translation->setId( $dbw->insertId() ); |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * @param SectionTranslation[] $translations |
| 55 | * @return void |
| 56 | */ |
| 57 | public function insertMultipleTranslations( array $translations ): void { |
| 58 | if ( !$translations ) { |
| 59 | return; |
| 60 | } |
| 61 | |
| 62 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 63 | $rows = []; |
| 64 | foreach ( $translations as $translation ) { |
| 65 | $values = $this->translationToDBRow( $translation ); |
| 66 | // set start/last_updated timestamps to current timestamp |
| 67 | $values['cxsx_translation_start_timestamp'] = $dbw->timestamp(); |
| 68 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
| 69 | $rows[] = $values; |
| 70 | } |
| 71 | |
| 72 | $dbw->newInsertQueryBuilder() |
| 73 | ->insertInto( self::TABLE_NAME ) |
| 74 | ->rows( $rows ) |
| 75 | ->caller( __METHOD__ ) |
| 76 | ->execute(); |
| 77 | } |
| 78 | |
| 79 | public function updateTranslation( SectionTranslation $translation ) { |
| 80 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 81 | $values = $this->translationToDBRow( $translation ); |
| 82 | $values['cxsx_translation_last_updated_timestamp'] = $dbw->timestamp(); |
| 83 | |
| 84 | $dbw->newUpdateQueryBuilder() |
| 85 | ->update( self::TABLE_NAME ) |
| 86 | ->set( $values ) |
| 87 | ->where( [ 'cxsx_id' => $translation->getId() ] ) |
| 88 | ->caller( __METHOD__ ) |
| 89 | ->execute(); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * @param int|null $id The "cxsx_id" of the section translation row |
| 94 | * @param int|null $translationStatus Either "deleted", "draft" or "published" |
| 95 | */ |
| 96 | public function updateTranslationStatusById( ?int $id, ?int $translationStatus ) { |
| 97 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 98 | $dbw->newUpdateQueryBuilder() |
| 99 | ->update( self::TABLE_NAME ) |
| 100 | ->set( [ 'cxsx_translation_status' => $translationStatus ] ) |
| 101 | ->where( [ 'cxsx_id' => $id ] ) |
| 102 | ->caller( __METHOD__ ) |
| 103 | ->execute(); |
| 104 | } |
| 105 | |
| 106 | public function findTranslation( int $translationId, string $sectionId ): ?SectionTranslation { |
| 107 | $dbr = $this->connectionProvider->getReplicaDatabase(); |
| 108 | |
| 109 | $row = $dbr->newSelectQueryBuilder() |
| 110 | ->select( IDatabase::ALL_ROWS ) |
| 111 | ->from( self::TABLE_NAME ) |
| 112 | ->where( [ |
| 113 | 'cxsx_translation_id' => $translationId, |
| 114 | 'cxsx_section_id' => $sectionId |
| 115 | ] ) |
| 116 | ->caller( __METHOD__ ) |
| 117 | ->fetchRow(); |
| 118 | return $row ? $this->createTranslationFromRow( $row ) : null; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * @param int $translationId The id of the parent translation (from "cx_translations" table) |
| 123 | * @param string $sectionTitle The source section title |
| 124 | * @return SectionTranslation|null |
| 125 | */ |
| 126 | public function findTranslationBySectionTitle( int $translationId, string $sectionTitle ): ?SectionTranslation { |
| 127 | $dbr = $this->connectionProvider->getReplicaDatabase(); |
| 128 | |
| 129 | $row = $dbr->newSelectQueryBuilder() |
| 130 | ->select( ISQLPlatform::ALL_ROWS ) |
| 131 | ->from( self::TABLE_NAME ) |
| 132 | ->where( [ |
| 133 | 'cxsx_translation_id' => $translationId, |
| 134 | 'cxsx_source_section_title' => $sectionTitle |
| 135 | ] ) |
| 136 | ->limit( 1 ) |
| 137 | ->caller( __METHOD__ ) |
| 138 | ->fetchRow(); |
| 139 | |
| 140 | return $row ? $this->createTranslationFromRow( $row ) : null; |
| 141 | } |
| 142 | |
| 143 | public function createTranslationFromRow( \stdClass $row ): SectionTranslation { |
| 144 | return new SectionTranslation( |
| 145 | (int)$row->cxsx_id, |
| 146 | (int)$row->cxsx_translation_id, |
| 147 | $row->cxsx_section_id, |
| 148 | $row->cxsx_source_section_title, |
| 149 | $row->cxsx_target_section_title, |
| 150 | isset( $row->cxsx_translation_status ) ? (int)$row->cxsx_translation_status : null, |
| 151 | $row->cxsx_translation_progress, |
| 152 | ); |
| 153 | } |
| 154 | |
| 155 | public static function getStatusIndexByStatus( string $status ): int { |
| 156 | $index = array_search( $status, self::TRANSLATION_STATUSES ); |
| 157 | |
| 158 | if ( $index === false ) { |
| 159 | throw new InvalidArgumentException( '[CX] Invalid status provided' ); |
| 160 | } |
| 161 | |
| 162 | return $index; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * @param int $userId User ID |
| 167 | * @param string|null $from |
| 168 | * @param string|null $to |
| 169 | * @param string|null $status The status of the translation. Either "draft" or "published" |
| 170 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
| 171 | * @param string|null $offset Offset condition (timestamp) |
| 172 | * @return IResultWrapper |
| 173 | */ |
| 174 | private function doFindTranslationsByUser( |
| 175 | int $userId, |
| 176 | ?string $from = null, |
| 177 | ?string $to = null, |
| 178 | ?string $status = null, |
| 179 | int $limit = 100, |
| 180 | ?string $offset = null |
| 181 | ): IResultWrapper { |
| 182 | $dbr = $this->connectionProvider->getReplicaDatabase(); |
| 183 | |
| 184 | $whereConditions = [ 'translation_started_by' => $userId ]; |
| 185 | |
| 186 | if ( $status !== null ) { |
| 187 | $statusIndex = self::getStatusIndexByStatus( $status ); |
| 188 | $whereConditions[] = $dbr->orExpr( [ |
| 189 | $dbr->orExpr( [ |
| 190 | $dbr->andExpr( [ 'translation_status' => $status, 'cxsx_translation_id' => null ] ), |
| 191 | $dbr->andExpr( [ |
| 192 | 'translation_status' => $status, |
| 193 | 'cxsx_source_section_title' => '__LEAD_SECTION__' ] |
| 194 | ) |
| 195 | ] ), |
| 196 | 'cxsx_translation_status' => $statusIndex |
| 197 | ] ); |
| 198 | } |
| 199 | if ( $from !== null ) { |
| 200 | $whereConditions['translation_source_language'] = $from; |
| 201 | } |
| 202 | if ( $to !== null ) { |
| 203 | $whereConditions['translation_target_language'] = $to; |
| 204 | } |
| 205 | if ( $offset !== null ) { |
| 206 | $whereConditions[] = $dbr->expr( 'translation_last_updated_timestamp', '<', $dbr->timestamp( $offset ) ); |
| 207 | } |
| 208 | |
| 209 | return $dbr->newSelectQueryBuilder() |
| 210 | ->select( ISQLPlatform::ALL_ROWS ) |
| 211 | ->from( TranslationStore::TRANSLATION_TABLE_NAME ) |
| 212 | ->leftJoin( self::TABLE_NAME, null, 'translation_id = cxsx_translation_id' ) |
| 213 | ->where( $whereConditions ) |
| 214 | ->orderBy( 'translation_last_updated_timestamp', SelectQueryBuilder::SORT_DESC ) |
| 215 | ->limit( $limit ) |
| 216 | ->caller( __METHOD__ ) |
| 217 | ->fetchResultSet(); |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * @param int $userId User ID |
| 222 | * @param string|null $from |
| 223 | * @param string|null $to |
| 224 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
| 225 | * @param string|null $offset Offset condition (timestamp) |
| 226 | * @return DraftTranslationDTO[] |
| 227 | */ |
| 228 | public function findDraftSectionTranslationsByUser( |
| 229 | int $userId, |
| 230 | ?string $from = null, |
| 231 | ?string $to = null, |
| 232 | int $limit = 100, |
| 233 | ?string $offset = null |
| 234 | ): array { |
| 235 | $resultSet = $this->doFindTranslationsByUser( |
| 236 | $userId, |
| 237 | $from, |
| 238 | $to, |
| 239 | self::TRANSLATION_STATUS_DRAFT, |
| 240 | $limit, |
| 241 | $offset |
| 242 | ); |
| 243 | |
| 244 | $result = []; |
| 245 | foreach ( $resultSet as $row ) { |
| 246 | $result[] = new DraftTranslationDTO( |
| 247 | $row->cxsx_id ? (int)$row->cxsx_id : null, |
| 248 | (int)$row->translation_id, |
| 249 | $row->cxsx_section_id ?? null, |
| 250 | $row->translation_source_title, |
| 251 | $row->translation_source_language, |
| 252 | $row->translation_target_language, |
| 253 | $row->translation_start_timestamp, |
| 254 | $row->translation_last_updated_timestamp, |
| 255 | self::TRANSLATION_STATUSES[ $row->cxsx_translation_status ] ?? $row->translation_status, |
| 256 | $row->translation_source_revision_id, |
| 257 | $row->cxsx_translation_progress ?? $row->translation_progress, |
| 258 | $row->translation_target_title, |
| 259 | $row->cxsx_source_section_title, |
| 260 | $row->cxsx_target_section_title, |
| 261 | ); |
| 262 | } |
| 263 | |
| 264 | return $result; |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * @param int $userId User ID |
| 269 | * @param string|null $from |
| 270 | * @param string|null $to |
| 271 | * @param int $limit How many results to return. Defaults to 100, same as for the "contenttranslation" list API |
| 272 | * @param string|null $offset Offset condition (timestamp) |
| 273 | * @return PublishedTranslationDTO[] |
| 274 | */ |
| 275 | public function findPublishedSectionTranslationsByUser( |
| 276 | int $userId, |
| 277 | ?string $from = null, |
| 278 | ?string $to = null, |
| 279 | int $limit = 100, |
| 280 | ?string $offset = null |
| 281 | ): array { |
| 282 | $resultSet = $this->doFindTranslationsByUser( |
| 283 | $userId, |
| 284 | $from, |
| 285 | $to, |
| 286 | self::TRANSLATION_STATUS_PUBLISHED, |
| 287 | $limit, |
| 288 | $offset |
| 289 | ); |
| 290 | |
| 291 | /** |
| 292 | * An array of PublishedSectionTranslationDTO objects grouped by translation id |
| 293 | * @var PublishedSectionTranslationDTO[] $publishedSectionTranslations |
| 294 | */ |
| 295 | $publishedSectionTranslations = []; |
| 296 | $publishedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_PUBLISHED ); |
| 297 | |
| 298 | foreach ( $resultSet as $row ) { |
| 299 | $translationId = (int)$row->translation_id; |
| 300 | if ( $row->cxsx_id && $row->cxsx_translation_status === $publishedStatusIndex ) { |
| 301 | $publishedSectionTranslations[$translationId] = new PublishedSectionTranslationDTO( |
| 302 | (int)$row->cxsx_id, |
| 303 | $row->cxsx_section_id, |
| 304 | $row->cxsx_translation_start_timestamp, |
| 305 | $row->cxsx_translation_last_updated_timestamp, |
| 306 | $row->cxsx_source_section_title, |
| 307 | $row->cxsx_target_section_title, |
| 308 | ); |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | $result = []; |
| 313 | foreach ( $resultSet as $row ) { |
| 314 | $translationId = (int)$row->translation_id; |
| 315 | // multiple rows can exist for the same translation id |
| 316 | // we only need one DTO per translation |
| 317 | if ( isset( $result[$translationId] ) ) { |
| 318 | continue; |
| 319 | } |
| 320 | $result[$translationId] = new PublishedTranslationDTO( |
| 321 | $translationId, |
| 322 | $row->translation_source_title, |
| 323 | $row->translation_source_language, |
| 324 | $row->translation_target_language, |
| 325 | $row->translation_start_timestamp, |
| 326 | $row->translation_last_updated_timestamp, |
| 327 | $row->translation_source_revision_id, |
| 328 | $row->translation_target_title, |
| 329 | $row->translation_target_url, |
| 330 | $publishedSectionTranslations[$translationId] ?? [] |
| 331 | ); |
| 332 | } |
| 333 | |
| 334 | return array_values( $result ); |
| 335 | } |
| 336 | |
| 337 | /** |
| 338 | * Given the "parent" translation id and the section id |
| 339 | * (in the "${revision}_${sectionNumber} form), this method |
| 340 | * deletes the corresponding section translation from the |
| 341 | * "cx_section_translations" table. |
| 342 | * |
| 343 | * @param int $sectionTranslationId |
| 344 | * @return void |
| 345 | */ |
| 346 | public function deleteTranslationById( int $sectionTranslationId ): void { |
| 347 | $dbw = $this->connectionProvider->getPrimaryDatabase(); |
| 348 | |
| 349 | $deletedStatusIndex = self::getStatusIndexByStatus( self::TRANSLATION_STATUS_DELETED ); |
| 350 | $dbw->newUpdateQueryBuilder() |
| 351 | ->update( self::TABLE_NAME ) |
| 352 | ->set( [ 'cxsx_translation_status' => $deletedStatusIndex ] ) |
| 353 | ->where( [ 'cxsx_id' => $sectionTranslationId ] ) |
| 354 | ->caller( __METHOD__ ) |
| 355 | ->execute(); |
| 356 | } |
| 357 | |
| 358 | private function translationToDBRow( SectionTranslation $translation ): array { |
| 359 | return [ |
| 360 | 'cxsx_translation_id' => $translation->getTranslationId(), |
| 361 | 'cxsx_section_id' => $translation->getSectionId(), |
| 362 | 'cxsx_source_section_title' => $translation->getSourceSectionTitle(), |
| 363 | 'cxsx_target_section_title' => $translation->getTargetSectionTitle(), |
| 364 | 'cxsx_translation_status' => $translation->getTranslationStatus(), |
| 365 | 'cxsx_translation_progress' => $translation->getProgress(), |
| 366 | ]; |
| 367 | } |
| 368 | } |