Completed
Push — master ( d0838b...120b58 )
by MusikAnimal
07:49 queued 02:25
created

Repository   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Test Coverage

Coverage 56.16%

Importance

Changes 0
Metric Value
dl 0
loc 293
ccs 41
cts 73
cp 0.5616
rs 8.6
c 0
b 0
f 0
wmc 37

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getMetaConnection() 0 9 2
D getTableName() 0 32 9
A __construct() 0 3 1
A getCacheKeyFromArg() 0 10 3
A getToolsConnection() 0 9 2
A getProjectsConnection() 0 9 2
A queryXToolsApi() 0 17 3
A isLabs() 0 3 1
A getMediawikiApi() 0 9 2
A getLog() 0 3 1
A setContainer() 0 6 1
A setCache() 0 7 1
B getCacheKey() 0 24 6
A createDatesConditions() 0 14 3
1
<?php
2
/**
3
 * This file contains only the Repository class.
4
 */
5
6
namespace Xtools;
7
8
use Doctrine\DBAL\Connection;
9
use Mediawiki\Api\MediawikiApi;
10
use Psr\Cache\CacheItemPoolInterface;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\NullLogger;
13
use Symfony\Component\DependencyInjection\Container;
14
use Symfony\Component\Stopwatch\Stopwatch;
15
use GuzzleHttp\Promise\Promise;
16
use DateInterval;
17
18
/**
19
 * A repository is responsible for retrieving data from wherever it lives (databases, APIs,
20
 * filesystems, etc.)
21
 */
22
abstract class Repository
23
{
24
25
    /** @var Container The application's DI container. */
26
    protected $container;
27
28
    /** @var Connection The database connection to the meta database. */
29
    private $metaConnection;
30
31
    /** @var Connection The database connection to the projects' databases. */
32
    private $projectsConnection;
33
34
    /** @var Connection The database connection to other tools' databases.  */
35
    private $toolsConnection;
36
37
    /** @var GuzzleHttp\Client $apiConnection Connection to XTools API. */
0 ignored issues
show
Bug introduced by
The type Xtools\GuzzleHttp\Client was not found. Did you mean GuzzleHttp\Client? If so, make sure to prefix the type with \.
Loading history...
38
    private $apiConnection;
39
40
    /** @var CacheItemPoolInterface The cache. */
41
    protected $cache;
42
43
    /** @var LoggerInterface The log. */
44
    protected $log;
45
46
    /** @var Stopwatch The stopwatch for time profiling. */
47
    protected $stopwatch;
48
49
    /**
50
     * Create a new Repository with nothing but a null-logger.
51
     */
52 94
    public function __construct()
53
    {
54 94
        $this->log = new NullLogger();
55 94
    }
56
57
    /**
58
     * Set the DI container.
59
     * @param Container $container
60
     */
61 15
    public function setContainer(Container $container)
62
    {
63 15
        $this->container = $container;
64 15
        $this->cache = $container->get('cache.app');
65 15
        $this->log = $container->get('logger');
66 15
        $this->stopwatch = $container->get('debug.stopwatch');
67 15
    }
68
69
    /**
70
     * Get the NullLogger instance.
71
     * @return NullLogger
72
     */
73 1
    public function getLog()
74
    {
75 1
        return $this->log;
76
    }
77
78
    /**
79
     * Get the database connection for the 'meta' database.
80
     * @return Connection
81
     * @codeCoverageIgnore
82
     */
83
    protected function getMetaConnection()
84
    {
85
        if (!$this->metaConnection instanceof Connection) {
86
            $this->metaConnection = $this->container
87
                ->get('doctrine')
88
                ->getManager('meta')
89
                ->getConnection();
90
        }
91
        return $this->metaConnection;
92
    }
93
94
    /**
95
     * Get the database connection for the 'projects' database.
96
     * @return Connection
97
     * @codeCoverageIgnore
98
     */
99
    protected function getProjectsConnection()
100
    {
101
        if (!$this->projectsConnection instanceof Connection) {
102
            $this->projectsConnection = $this->container
103
                ->get('doctrine')
104
                ->getManager('replicas')
105
                ->getConnection();
106
        }
107
        return $this->projectsConnection;
108
    }
109
110
    /**
111
     * Get the database connection for the 'tools' database
112
     * (the one that other tools store data in).
113
     * @return Connection
114
     * @codeCoverageIgnore
115
     */
116
    protected function getToolsConnection()
117
    {
118
        if (!$this->toolsConnection instanceof Connection) {
119
            $this->toolsConnection = $this->container
120
                ->get('doctrine')
121
                ->getManager("toolsdb")
122
                ->getConnection();
123
        }
124
        return $this->toolsConnection;
125
    }
126
127
    /**
128
     * Get the API object for the given project.
129
     *
130
     * @param Project $project
131
     * @return MediawikiApi
132
     */
133 2
    public function getMediawikiApi(Project $project)
134
    {
135 2
        $apiPath = $this->container->getParameter('api_path');
136 2
        if ($apiPath) {
137 2
            $api = MediawikiApi::newFromApiEndpoint($project->getUrl().$apiPath);
138
        } else {
139
            $api = MediawikiApi::newFromPage($project->getUrl());
140
        }
141 2
        return $api;
142
    }
143
144
    /**
145
     * Is XTools connecting to MMF Labs?
146
     * @return boolean
147
     * @codeCoverageIgnore
148
     */
149
    public function isLabs()
150
    {
151
        return (bool)$this->container->getParameter('app.is_labs');
152
    }
153
154
    /**
155
     * Make a request to the XTools API, optionally doing so asynchronously via Guzzle.
156
     * @param string $endpoint Relative path to endpoint with relevant query parameters.
157
     * @param bool $async Set to true to asynchronously query and return a promise.
158
     * @return GuzzleHttp\Psr7\Response|GuzzleHttp\Promise\Promise
0 ignored issues
show
Bug introduced by
The type Xtools\GuzzleHttp\Promise\Promise was not found. Did you mean GuzzleHttp\Promise\Promise? If so, make sure to prefix the type with \.
Loading history...
Bug introduced by
The type Xtools\GuzzleHttp\Psr7\Response was not found. Did you mean GuzzleHttp\Psr7\Response? If so, make sure to prefix the type with \.
Loading history...
159
     */
160
    public function queryXToolsApi($endpoint, $async = false)
161
    {
162
        if (!$this->apiConnection) {
163
            $this->apiConnection = $this->container->get('guzzle.client.xtools');
164
        }
165
166
        $key = $this->container->getParameter('secret');
167
168
        // Remove trailing slash if present.
169
        $basePath = trim($this->container->getParameter('app.base_path'), '/');
170
171
        $endpoint = "$basePath/api/$endpoint/$key";
172
173
        if ($async) {
174
            return $this->apiConnection->getAsync($endpoint);
175
        } else {
176
            return $this->apiConnection->get($endpoint);
177
        }
178
    }
179
180
    /**
181
     * Normalize and quote a table name for use in SQL.
182
     *
183
     * @param string $databaseName
184
     * @param string $tableName
185
     * @param string|null $tableExtension Optional table extension, which will only get used if we're on labs.
186
     * @return string Fully-qualified and quoted table name.
187
     */
188 1
    public function getTableName($databaseName, $tableName, $tableExtension = null)
189
    {
190 1
        $mapped = false;
191
192
        // This is a workaround for a one-to-many mapping
193
        // as required by Labs. We combine $tableName with
194
        // $tableExtension in order to generate the new table name
195 1
        if ($this->isLabs() && $tableExtension !== null) {
196
            $mapped = true;
197
            $tableName = $tableName . '_' . $tableExtension;
198 1
        } elseif ($this->container->hasParameter("app.table.$tableName")) {
199
            // Use the table specified in the table mapping configuration, if present.
200
            $mapped = true;
201
            $tableName = $this->container->getParameter("app.table.$tableName");
202
        }
203
204
        // For 'revision' and 'logging' tables (actually views) on Labs, use the indexed versions
205
        // (that have some rows hidden, e.g. for revdeleted users).
206
        // This is a safeguard in case table mapping isn't properly set up.
207 1
        $isLoggingOrRevision = in_array($tableName, ['revision', 'logging', 'archive']);
208 1
        if (!$mapped && $isLoggingOrRevision && $this->isLabs()) {
209
            $tableName = $tableName."_userindex";
210
        }
211
212
        // Figure out database name.
213
        // Use class variable for the database name if not set via function parameter.
214 1
        if ($this->isLabs() && substr($databaseName, -2) != '_p') {
215
            // Append '_p' if this is labs.
216
            $databaseName .= '_p';
217
        }
218
219 1
        return "`$databaseName`.`$tableName`";
220
    }
221
222
    /**
223
     * Get a unique cache key for the given list of arguments. Assuming each argument of
224
     * your function should be accounted for, you can pass in them all with func_get_args:
225
     *   $this->getCacheKey(func_get_args(), 'unique key for function');
226
     * Arugments that are a model should implement their own getCacheKey() that returns
227
     * a unique identifier for an instance of that model. See User::getCacheKey() for example.
228
     * @param array|mixed $args Array of arguments or a single argument.
229
     * @param string $key Unique key for this function. If omitted the function name itself
230
     *   is used, which is determined using `debug_backtrace`.
231
     * @return string
232
     */
233 7
    public function getCacheKey($args, $key = null)
234
    {
235 7
        if ($key === null) {
236 1
            $key = debug_backtrace()[1]['function'];
237
        }
238
239 7
        if (!is_array($args)) {
240 7
            $args = [$args];
241
        }
242
243
        // Start with base key.
244 7
        $cacheKey = $key;
245
246
        // Loop through and determine what values to use based on type of object.
247 7
        foreach ($args as $arg) {
248
            // Zero is an acceptable value.
249 7
            if ($arg === '' || $arg === null) {
250 1
                continue;
251
            }
252
253 7
            $cacheKey .= $this->getCacheKeyFromArg($arg);
254
        }
255
256 7
        return $cacheKey;
257
    }
258
259
    /**
260
     * Get a cache-friendly string given an argument.
261
     * @param  mixed $arg
262
     * @return string
263
     */
264 7
    private function getCacheKeyFromArg($arg)
265
    {
266 7
        if (method_exists($arg, 'getCacheKey')) {
267 1
            return '.'.$arg->getCacheKey();
268 7
        } elseif (is_array($arg)) {
269
            // Assumed to be an array of objects that can be parsed into a string.
270 1
            return '.'.join('', $arg);
271
        } else {
272
            // Assumed to be a string, number or boolean.
273 7
            return '.'.md5($arg);
274
        }
275
    }
276
277
    /**
278
     * Set the cache with given options.
279
     * @param string $cacheKey
280
     * @param mixed  $value
281
     * @param string $duration Valid DateInterval string.
282
     */
283
    public function setCache($cacheKey, $value, $duration = 'PT10M')
284
    {
285
        $cacheItem = $this->cache
286
            ->getItem($cacheKey)
287
            ->set($value)
288
            ->expiresAfter(new DateInterval($duration));
289
        $this->cache->save($cacheItem);
290
    }
291
292
    /**
293
     * Creates WHERE conditions with date range to be put in query.
294
     *
295
     * @param false|int $start
296
     * @param false|int $end
297
     * @param string $tableAlias Alias of table FOLLOWED BY DOT.
298
     * @param string $field
299
     * @return string
300
     */
301
    public function createDatesConditions($start, $end, $tableAlias = '', $field = 'rev_timestamp')
302
    {
303
        $datesConditions = '';
304
        if (false !== $start) {
305
            // Convert to YYYYMMDDHHMMSS. *who in the world thought of having time in BLOB of this format ;-;*
306
            $start = date('Ymd', $start) . '000000';
307
            $datesConditions .= " AND {$tableAlias}{$field} > '$start'";
308
        }
309
        if (false !== $end) {
310
            $end = date('Ymd', $end) . '000000';
311
            $datesConditions .= " AND {$tableAlias}{$field} < '$end'";
312
        }
313
314
        return $datesConditions;
315
    }
316
}
317