Passed
Push — master ( 8b9089...a51f69 )
by MusikAnimal
01:23
created

Repository   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 255
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 4

Test Coverage

Coverage 71.93%

Importance

Changes 0
Metric Value
wmc 33
lcom 4
cbo 4
dl 0
loc 255
rs 9.3999
c 0
b 0
f 0
ccs 41
cts 57
cp 0.7193

12 Methods

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