Passed
Push — master ( c7c9c7...1a5f01 )
by Aimeos
02:25
created

DB::getMultiple()   B

Complexity

Conditions 7
Paths 50

Size

Total Lines 41
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 21
c 2
b 0
f 0
dl 0
loc 41
rs 8.6506
cc 7
nc 50
nop 2
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Aimeos (aimeos.org), 2015-2022
6
 * @package Base
7
 * @subpackage Cache
8
 */
9
10
11
namespace Aimeos\Base\Cache;
12
13
14
/**
15
 * Database cache class.
16
 *
17
 * @package Base
18
 * @subpackage Cache
19
 */
20
class DB
21
	extends \Aimeos\Base\Cache\Base
22
	implements \Aimeos\Base\Cache\Iface
23
{
24
	private $conn;
25
	private $sql;
26
27
28
	/**
29
	 * Initializes the object instance.
30
	 *
31
	 * The config array must contain these statement:
32
	 *	[delete] =>
33
	 *		DELETE FROM cachetable WHERE :cond
34
	 *	[deletebytag] =>
35
	 *		DELETE FROM cachetable WHERE id IN (
36
	 *			SELECT tid FROM cachetagtable WHERE :cond
37
	 *		)
38
	 *	[get] =>
39
	 *		SELECT id, value, expire FROM cachetable WHERE :cond
40
	 *	[set] =>
41
	 *		INSERT INTO cachetable ( id, expire, value ) VALUES ( ?, ?, ? )
42
	 *	[settag] =>
43
	 *		INSERT INTO cachetagtable ( tid, tname ) VALUES ( ?, ? )
44
	 *
45
	 * For using a different database connection, the name of the database connection
46
	 * can be also given in the "config" parameter. In this case, use e.g.
47
	 *  config['dbname'] = 'db-cache'
48
	 *
49
	 * @param array $config Associative list with SQL statements
50
	 * @param \Aimeos\Base\DB\Connection\Iface $conn Database connection
51
	 */
52
	public function __construct( array $config, \Aimeos\Base\DB\Connection\Iface $conn )
53
	{
54
		$this->sql = $config;
55
		$this->conn = $conn;
56
	}
57
58
59
	/**
60
	 * Removes all expired cache entries.
61
	 *
62
	 * @inheritDoc
63
	 *
64
	 * @return bool True on success and false on failure
65
	 */
66
	public function cleanup() : bool
67
	{
68
		try
69
		{
70
			$this->conn->create( $this->sql( 'cleanup' ) )
71
				->bind( 1, date( 'Y-m-d H:i:00' ) )
72
				->execute()->finish();
73
		}
74
		catch( \Exception $e )
75
		{
76
			error_log( __METHOD__ . ': ' . $e->getMessage() );
77
			return false;
78
		}
79
80
		return true;
81
	}
82
83
84
	/**
85
	 * Removes all entries of the site from the cache.
86
	 *
87
	 * @inheritDoc
88
	 *
89
	 * @return bool True on success and false on failure
90
	 */
91
	public function clear() : bool
92
	{
93
		try
94
		{
95
			$this->conn->create( $this->sql( 'clear' ) )->execute()->finish();
96
		}
97
		catch( \Exception $e )
98
		{
99
			error_log( __METHOD__ . ': ' . $e->getMessage() );
100
			return false;
101
		}
102
103
		return true;
104
	}
105
106
107
	/**
108
	 * Removes the cache entries identified by the given keys.
109
	 *
110
	 * @inheritDoc
111
	 *
112
	 * @param iterable $keys List of key strings that identify the cache entries that should be removed
113
	 * @return bool True if the items were successfully removed. False if there was an error.
114
	 * @throws \Psr\SimpleCache\InvalidArgumentException
115
	 */
116
	public function deleteMultiple( iterable $keys ) : bool
117
	{
118
		try
119
		{
120
			if( ( $cnt = count( $keys ) ) === 0 ) {
121
				return true;
122
			}
123
124
			$pos = 1;
125
			$sql = $this->sql( 'delete' );
126
			$sql = substr_replace( $sql, str_repeat( ',?', $cnt - 1 ), strrpos( $sql, '?' ) + 1, 0 );
127
128
			$stmt = $this->conn->create( $sql );
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\Base\DB\Connection\Iface::create() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

128
			$stmt = $this->conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
129
130
			foreach( $keys as $key ) {
131
				$stmt->bind( $pos++, $key );
132
			}
133
134
			$stmt->execute()->finish();
135
		}
136
		catch( \Exception $e )
137
		{
138
			error_log( __METHOD__ . ': ' . $e->getMessage() );
139
			return false;
140
		}
141
142
		return true;
143
	}
144
145
146
	/**
147
	 * Removes the cache entries identified by the given tags.
148
	 *
149
	 * @inheritDoc
150
	 *
151
	 * @param iterable $tags List of tag strings that are associated to one or
152
	 *  more cache entries that should be removed
153
	 * @return bool True if the items were successfully removed. False if there was an error.
154
	 * @throws \Psr\SimpleCache\InvalidArgumentException
155
	 */
156
	public function deleteByTags( iterable $tags ) : bool
157
	{
158
		try
159
		{
160
			if( ( $cnt = count( $tags ) ) === 0 ) {
161
				return true;
162
			}
163
164
			$pos = 1;
165
			$sql = $this->sql( 'deletebytag' );
166
			$sql = substr_replace( $sql, str_repeat( ',?', $cnt - 1 ), strrpos( $sql, '?' ) + 1, 0 );
167
168
			$stmt = $this->conn->create( $sql );
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\Base\DB\Connection\Iface::create() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
			$stmt = $this->conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
169
170
			foreach( $tags as $tag ) {
171
				$stmt->bind( $pos++, $tag );
172
			}
173
174
			$stmt->execute()->finish();
175
		}
176
		catch( \Exception $e )
177
		{
178
			error_log( __METHOD__ . ': ' . $e->getMessage() );
179
			return false;
180
		}
181
182
		return true;
183
	}
184
185
186
	/**
187
	 * Returns the cached values for the given cache keys if available.
188
	 *
189
	 * @inheritDoc
190
	 *
191
	 * @param string[] $keys List of key strings for the requested cache entries
192
	 * @param mixed $default Default value to return for keys that do not exist
193
	 * @return iterable Associative list of key/value pairs for the requested cache
194
	 * 	entries. If a cache entry doesn't exist, neither its key nor a value
195
	 * 	will be in the result list
196
	 * @throws \Aimeos\Base\Cache\Exception If the cache server doesn't respond
197
	 */
198
	public function getMultiple( iterable $keys, $default = null ) : iterable
199
	{
200
		try
201
		{
202
			if( ( $cnt = count( $keys ) ) === 0 ) {
203
				return true;
204
			}
205
206
			$pos = 2;
207
			$list = [];
208
209
			$sql = $this->sql( 'get' );
210
			$sql = substr_replace( $sql, str_repeat( ',?', $cnt - 1 ), strrpos( $sql, '?' ) + 1, 0 );
211
212
			$stmt = $this->conn->create( $sql )
0 ignored issues
show
Bug introduced by
It seems like $sql can also be of type array; however, parameter $sql of Aimeos\Base\DB\Connection\Iface::create() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
			$stmt = $this->conn->create( /** @scrutinizer ignore-type */ $sql )
Loading history...
213
				->bind( 1, date( 'Y-m-d H:i:00' ) );
214
215
			foreach( $keys as $key ) {
216
				$stmt->bind( $pos++, $key );
217
			}
218
219
			$result = $stmt->execute();
220
221
			while( ( $row = $result->fetch() ) !== null ) {
222
				$list[$row['id']] = (string) $row['value'];
223
			}
224
225
			foreach( $keys as $key )
226
			{
227
				if( !isset( $list[$key] ) ) {
228
					$list[$key] = $default;
229
				}
230
			}
231
		}
232
		catch( \Exception $e )
233
		{
234
			error_log( __METHOD__ . ': ' . $e->getMessage() );
235
			return [];
236
		}
237
238
		return $list;
239
	}
240
241
242
	/**
243
	 * Determines whether an item is present in the cache.
244
	 *
245
	 * @inheritDoc
246
	 *
247
	 * @param string $key The cache item key
248
	 * @return bool True if cache entry is available, false if not
249
	 * @throws \Psr\SimpleCache\InvalidArgumentException
250
	 */
251
	public function has( string $key ) : bool
252
	{
253
		try
254
		{
255
			$return = false;
256
			$result = $this->conn->create( $this->sql( 'get' ) )
257
				->bind( 1, date( 'Y-m-d H:i:00' ) )
258
				->bind( 2, $key )
259
				->execute();
260
261
			while( $result->fetch() ) {
262
				$return = true;
263
			}
264
		}
265
		catch( \Exception $e )
266
		{
267
			error_log( __METHOD__ . ': ' . $e->getMessage() );
268
			return false;
269
		}
270
271
		return $return;
272
	}
273
274
275
	/**
276
	 * Adds or overwrites the given key/value pairs in the cache, which is much
277
	 * more efficient than setting them one by one using the set() method.
278
	 *
279
	 * @inheritDoc
280
	 *
281
	 * @param iterable $pairs Associative list of key/value pairs. Both must be a string
282
	 * @param \DateInterval|int|string|null $expires Date interval object,
283
	 *  date/time string in "YYYY-MM-DD HH:mm:ss" format or as integer TTL value
284
	 *  when the cache entry will expiry
285
	 * @param iterable $tags List of tags that should be associated to the cache entries
286
	 * @return bool True on success and false on failure.
287
	 * @throws \Psr\SimpleCache\InvalidArgumentException
288
	 */
289
	public function setMultiple( iterable $pairs, $expires = null, iterable $tags = [] ) : bool
290
	{
291
		$keys = [];
292
		foreach( $pairs as $key => $v ) {
293
			$keys[] = $key;
294
		}
295
296
		// Remove existing entries first to avoid duplicate key conflicts
297
		$this->deleteMultiple( $keys );
298
299
		try
300
		{
301
			$this->conn->begin();
302
			$stmt = $this->conn->create( $this->sql( 'set' ) );
303
			$stmtTag = $this->conn->create( $this->sql( 'settag' ) );
304
305
			foreach( $pairs as $key => $value )
306
			{
307
				if( $expires instanceof \DateInterval ) {
308
					$expires = date_create()->add( $expires )->format( 'Y-m-d H:i:s' );
309
				} elseif( is_int( $expires ) ) {
310
					$expires = date( 'Y-m-d H:i:s', time() + $expires );
311
				}
312
313
				$stmt->bind( 1, (string) $key );
314
				$stmt->bind( 2, $expires );
315
				$stmt->bind( 3, (string) $value );
316
				$stmt->execute()->finish();
317
318
				foreach( $tags as $name )
319
				{
320
					$stmtTag->bind( 1, (string) $key );
321
					$stmtTag->bind( 2, (string) $name );
322
					$stmtTag->execute()->finish();
323
				}
324
			}
325
326
			$this->conn->commit();
327
		}
328
		catch( \Exception $e )
329
		{
330
			$this->conn->rollback();
331
332
			error_log( __METHOD__ . ': ' . $e->getMessage() );
333
			return false;
334
		}
335
336
		return true;
337
	}
338
339
340
	/**
341
	 * Retturns the SQL statement for the given name.
342
	 *
343
	 * @param string $name SQL statement
344
	 * @throws \Aimeos\Base\Cache\Exception If SQL statement is not available
345
	 */
346
	protected function sql( string $name ) : string
347
	{
348
		if( isset( $this->sql[$name] ) ) {
349
			return $this->sql[$name];
350
		}
351
352
		throw new \Aimeos\Base\Cache\Exception( "SQL statement for $name is missing" );
353
	}
354
}
355