Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CategoryStore
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 5
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 save
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 update
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 create
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 exists
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare( strict_types = 1 );
4
5/**
6 * Category store for source and target categories in cx_corpora table. cx_corpora was initially
7 * used to store pairs of source and target (translated) sections, but extended to store pairs
8 * of source and target category lists, which utilize special section ID of 'CX_CATEGORY_METADATA'.
9 *
10 * @copyright See AUTHORS.txt
11 * @license GPL-2.0-or-later
12 */
13
14namespace ContentTranslation;
15
16use Wikimedia\Rdbms\IConnectionProvider;
17
18class CategoryStore {
19    private const TYPE_SOURCE = 'source';
20    private const TYPE_USER = 'user';
21
22    private const CATEGORIES_SECTION = [
23        'cxc_section_id' => 'CX_CATEGORY_METADATA'
24    ];
25
26    public function __construct( private readonly IConnectionProvider $connectionProvider ) {
27    }
28
29    /**
30     * Save the source and target category list.
31     * If the record exists, update it, otherwise create.
32     */
33    public function save(
34        int $translationId,
35        ?string $sourceCategories,
36        string $targetCategories,
37        bool $newTranslation
38    ): void {
39        $db = $this->connectionProvider->getPrimaryDatabase();
40
41        $db->doAtomicSection(
42            __METHOD__,
43            function () use (
44                $translationId, $sourceCategories, $targetCategories, $newTranslation
45            ) {
46                if ( $newTranslation ) {
47                    $existing = false;
48                } else {
49                    $existing = $this->exists( $translationId );
50                }
51
52                if ( $existing ) {
53                    $this->update( $translationId, $sourceCategories, self::TYPE_SOURCE );
54                    $this->update( $translationId, $targetCategories, self::TYPE_USER );
55                } else {
56                    $this->create( $translationId, $sourceCategories, $targetCategories );
57                }
58            }
59        );
60    }
61
62    /**
63     * Update source or target categories. Since API doesn't require source nor target categories
64     * while performing saving of a draft, if $categories isn't provided, do nothing.
65     */
66    private function update(
67        int $translationId,
68        ?string $categories,
69        string $origin
70    ): void {
71        if ( !$categories ) {
72            return;
73        }
74
75        $db = $this->connectionProvider->getPrimaryDatabase();
76        $values = [
77            'cxc_timestamp' => $db->timestamp(),
78            'cxc_content' => $categories
79        ];
80        $conditions = [
81            'cxc_translation_id' => $translationId,
82            'cxc_origin' => $origin
83        ] + self::CATEGORIES_SECTION;
84
85        $db->newUpdateQueryBuilder()
86            ->update( 'cx_corpora' )
87            ->set( $values )
88            ->where( $conditions )
89            ->caller( __METHOD__ )
90            ->execute();
91    }
92
93    /**
94     * Insert source and target category list for the given translation identifier.
95     */
96    private function create(
97        int $translationId,
98        ?string $sourceCategories,
99        string $targetCategories
100    ): void {
101        $db = $this->connectionProvider->getPrimaryDatabase();
102
103        $values = [];
104        $commonValues = [
105            'cxc_translation_id' => $translationId,
106            'cxc_timestamp' => $db->timestamp()
107        ] + self::CATEGORIES_SECTION;
108
109        if ( $targetCategories ) {
110            $values[] = [
111                'cxc_origin' => self::TYPE_USER,
112                'cxc_content' => $targetCategories
113            ] + $commonValues;
114        }
115
116        if ( $sourceCategories ) {
117            $values[] = [
118                'cxc_origin' => self::TYPE_SOURCE,
119                'cxc_content' => $sourceCategories
120            ] + $commonValues;
121        }
122
123        if ( $values !== [] ) {
124            $db->newInsertQueryBuilder()
125                ->insertInto( 'cx_corpora' )
126                ->rows( $values )
127                ->caller( __METHOD__ )
128                ->execute();
129        }
130    }
131
132    /**
133     * Find if there are records about source and target categories.
134     */
135    private function exists( int $translationId ): bool {
136        $db = $this->connectionProvider->getPrimaryDatabase();
137
138        $result = $db->newSelectQueryBuilder()
139            ->select( 'cxc_content' )
140            ->from( 'cx_corpora' )
141            ->where( [ 'cxc_translation_id' => $translationId ] )
142            ->andWhere( self::CATEGORIES_SECTION )
143            ->forUpdate()
144            ->caller( __METHOD__ )
145            ->fetchResultSet();
146
147        return $result->numRows() > 0;
148    }
149}