Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.35% covered (warning)
82.35%
42 / 51
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageGroupWANCache
82.35% covered (warning)
82.35%
42 / 51
42.86% covered (danger)
42.86%
3 / 7
18.59
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getValue
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 setValue
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
1.12
 delete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 configure
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
5.34
 checkConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 toClosure
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use Closure;
7use InvalidArgumentException;
8use TypeError;
9use WANObjectCache;
10
11/**
12 * A wrapper around WANObjectCache providing a simpler interface for
13 * MessageGroups to use the cache.
14 *
15 * @author Abijeet Patro
16 * @license GPL-2.0-or-later
17 */
18class MessageGroupWANCache {
19    private WANObjectCache $cache;
20    private string $cacheKey;
21    /**
22     * To be called when the cache is empty or expired to get the data
23     * to repopulate the cache
24     */
25    private Closure $regenerator;
26    /** @see @https://doc.wikimedia.org/mediawiki-core/master/php/classWANObjectCache.html */
27    private int $lockTSE;
28    /** @see @https://doc.wikimedia.org/mediawiki-core/master/php/classWANObjectCache.html */
29    private ?Closure $touchedCallback;
30    /** @see @https://doc.wikimedia.org/mediawiki-core/master/php/classWANObjectCache.html */
31    private int $ttl;
32
33    /**
34     * A prefix for all keys saved by this cache
35     * @var string
36     */
37    private const KEY_PREFIX = 'translate-mg';
38
39    public function __construct( WANObjectCache $cache ) {
40        $this->cache = $cache;
41    }
42
43    /**
44     * Fetches value from cache for a message group.
45     *
46     * @param string|false $recache Either "recache" or false
47     * @return mixed
48     */
49    public function getValue( $recache = false ) {
50        $this->checkConfig();
51
52        return $this->cache->getWithSetCallback(
53            $this->cacheKey,
54            $this->ttl,
55            $this->regenerator,
56            [
57                'lockTSE' => $this->lockTSE, // avoid stampedes (mutex)
58                'touchedCallback' => function ( $value ) {
59                    if ( isset( $this->touchedCallback ) && call_user_func( $this->touchedCallback, $value ) ) {
60                        // treat value as if it just expired (for "lockTSE")
61                        return time();
62                    }
63
64                    return null;
65                },
66                // "miss" on recache
67                'minAsOf' => $recache ? INF : WANObjectCache::MIN_TIMESTAMP_NONE,
68            ]
69        );
70    }
71
72    /**
73     * Sets value in the cache for the message group
74     * @param mixed $cacheData
75     */
76    public function setValue( $cacheData ): void {
77        $this->checkConfig();
78        $this->cache->set( $this->cacheKey, $cacheData, $this->ttl );
79    }
80
81    /** Deletes the cached value */
82    public function delete(): void {
83        $this->checkConfig();
84        $this->cache->delete( $this->cacheKey );
85    }
86
87    /** Configure the message group. This must be called before making a call to any other method. */
88    public function configure( array $config ): void {
89        if ( !isset( $config['key'] ) ) {
90                throw new InvalidArgumentException( '$config[\'key\'] not set' );
91        }
92        $cacheKey = $config['key'];
93
94        if ( !isset( $config['regenerator'] ) ) {
95                throw new InvalidArgumentException( '$config[\'regenerator\'] not set' );
96        }
97        $this->regenerator = $this->toClosure( 'regenerator', $config['regenerator'] );
98
99        $cacheVersion = $config['version'] ?? null;
100        $this->lockTSE = $config['lockTSE'] ?? 30;
101        $this->touchedCallback = isset( $config['touchedCallback'] )
102            ? $this->toClosure( 'touchedCallback', $config['touchedCallback'] )
103            : null;
104
105        $this->ttl = $config['ttl'] ?? WANObjectCache::TTL_DAY;
106
107        if ( $cacheVersion ) {
108            $this->cacheKey = $this->cache->makeKey(
109                self::KEY_PREFIX,
110                strtolower( $cacheKey ),
111                'v' . $cacheVersion
112            );
113        } else {
114            $this->cacheKey = $this->cache->makeKey(
115                self::KEY_PREFIX, strtolower( $cacheKey )
116            );
117        }
118    }
119
120    /** Check to see if the instance is configured properly. */
121    private function checkConfig(): void {
122        // Attempt to cast $this->regenerator of type \Closure to isset
123        // @phan-suppress-next-line PhanRedundantCondition
124        if ( !isset( $this->cacheKey ) || !isset( $this->regenerator ) ) {
125            throw new InvalidArgumentException(
126                "Required data not set."
127                . " Ensure you have called the configure function before get / setting values."
128            );
129        }
130    }
131
132    /**
133     * @param string $key key of the config
134     * @param mixed $potentialCallable The potential callable to convert.
135     */
136    private function toClosure( string $key, $potentialCallable ): Closure {
137        try {
138            return Closure::fromCallable( $potentialCallable );
139        } catch ( TypeError $e ) {
140            throw new InvalidArgumentException(
141                "\$config['$key'] is not callable: " . $e->getMessage(), 0, $e
142            );
143        }
144    }
145}