Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LCStoreDB
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 6
462
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
 get
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 startWrite
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 finishWrite
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 set
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getWriteConnection
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\MediaWikiServices;
22use Wikimedia\Rdbms\DBQueryError;
23use Wikimedia\Rdbms\IDatabase;
24use Wikimedia\ScopedCallback;
25
26/**
27 * LCStore implementation which uses the standard DB functions to store data.
28 *
29 * @ingroup Language
30 */
31class LCStoreDB implements LCStore {
32    /** @var string|null Language code */
33    private $code;
34    /** @var array Server configuration map */
35    private $server;
36
37    /** @var array Rows buffered for insertion */
38    private $batch = [];
39
40    /** @var IDatabase|null */
41    private $dbw;
42    /** @var bool Whether write batch was recently written */
43    private $writesDone = false;
44    /** @var bool Whether the DB is read-only or otherwise unavailable for writing */
45    private $readOnly = false;
46
47    public function __construct( $params ) {
48        $this->server = $params['server'] ?? [];
49    }
50
51    public function get( $code, $key ) {
52        if ( $this->server || $this->writesDone ) {
53            // If a server configuration map is specified, always used that connection
54            // for reads and writes. Otherwise, if writes occurred in finishWrite(), make
55            // sure those changes are always visible.
56            $db = $this->getWriteConnection();
57        } else {
58            $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
59        }
60
61        $value = $db->newSelectQueryBuilder()
62            ->select( 'lc_value' )
63            ->from( 'l10n_cache' )
64            ->where( [ 'lc_lang' => $code, 'lc_key' => $key ] )
65            ->caller( __METHOD__ )->fetchField();
66
67        return ( $value !== false ) ? unserialize( $db->decodeBlob( $value ) ) : null;
68    }
69
70    public function startWrite( $code ) {
71        if ( $this->readOnly ) {
72            return;
73        } elseif ( !$code ) {
74            throw new InvalidArgumentException( __METHOD__ . ": Invalid language \"$code\"" );
75        }
76
77        $dbw = $this->getWriteConnection();
78        $this->readOnly = $dbw->isReadOnly();
79
80        $this->code = $code;
81        $this->batch = [];
82    }
83
84    public function finishWrite() {
85        if ( $this->readOnly ) {
86            return;
87        } elseif ( $this->code === null ) {
88            throw new LogicException( __CLASS__ . ': must call startWrite() before finishWrite()' );
89        }
90
91        $scope = Profiler::instance()->getTransactionProfiler()->silenceForScope();
92        $dbw = $this->getWriteConnection();
93        $dbw->startAtomic( __METHOD__ );
94        try {
95            $dbw->newDeleteQueryBuilder()
96                ->deleteFrom( 'l10n_cache' )
97                ->where( [ 'lc_lang' => $this->code ] )
98                ->caller( __METHOD__ )->execute();
99            foreach ( array_chunk( $this->batch, 500 ) as $rows ) {
100                $dbw->newInsertQueryBuilder()
101                    ->insertInto( 'l10n_cache' )
102                    ->rows( $rows )
103                    ->caller( __METHOD__ )->execute();
104            }
105            $this->writesDone = true;
106        } catch ( DBQueryError $e ) {
107            if ( $dbw->wasReadOnlyError() ) {
108                $this->readOnly = true; // just avoid site downtime
109            } else {
110                throw $e;
111            }
112        }
113        $dbw->endAtomic( __METHOD__ );
114        ScopedCallback::consume( $scope );
115
116        $this->code = null;
117        $this->batch = [];
118    }
119
120    public function set( $key, $value ) {
121        if ( $this->readOnly ) {
122            return;
123        } elseif ( $this->code === null ) {
124            throw new LogicException( __CLASS__ . ': must call startWrite() before set()' );
125        }
126
127        $dbw = $this->getWriteConnection();
128
129        $this->batch[] = [
130            'lc_lang' => $this->code,
131            'lc_key' => $key,
132            'lc_value' => $dbw->encodeBlob( serialize( $value ) )
133        ];
134    }
135
136    /**
137     * @return IDatabase
138     */
139    private function getWriteConnection() {
140        if ( !$this->dbw ) {
141            if ( $this->server ) {
142                $dbFactory = MediaWikiServices::getInstance()->getDatabaseFactory();
143                $this->dbw = $dbFactory->create( $this->server['type'], $this->server );
144                if ( !$this->dbw ) {
145                    throw new RuntimeException( __CLASS__ . ': failed to obtain a DB connection' );
146                }
147            } else {
148                $this->dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
149            }
150        }
151
152        return $this->dbw;
153    }
154}