Passed
Push — master ( c290d5...c7c9c7 )
by Aimeos
02:21
created

DB::setMultiple()   B

Complexity

Conditions 7
Paths 58

Size

Total Lines 48
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 48
rs 8.5546
cc 7
nc 58
nop 3
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
		if( ( $cnt = count( $keys ) ) === 0 ) {
119
			return true;
120
		}
121
122
		try
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
			$pos = 1;
161
			$sql = $this->sql( 'deletebytag' );
162
			$sql = substr_replace( $sql, str_repeat( ',?', count( $tags ) - 1 ), strrpos( $sql, '?' ) + 1, 0 );
163
164
			$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

164
			$stmt = $this->conn->create( /** @scrutinizer ignore-type */ $sql );
Loading history...
165
166
			foreach( $tags as $tag ) {
167
				$stmt->bind( $pos++, $tag )->execute()->finish();
168
			}
169
		}
170
		catch( \Exception $e )
171
		{
172
			error_log( __METHOD__ . ': ' . $e->getMessage() );
173
			return false;
174
		}
175
176
		return true;
177
	}
178
179
180
	/**
181
	 * Returns the cached values for the given cache keys if available.
182
	 *
183
	 * @inheritDoc
184
	 *
185
	 * @param string[] $keys List of key strings for the requested cache entries
186
	 * @param mixed $default Default value to return for keys that do not exist
187
	 * @return iterable Associative list of key/value pairs for the requested cache
188
	 * 	entries. If a cache entry doesn't exist, neither its key nor a value
189
	 * 	will be in the result list
190
	 * @throws \Aimeos\Base\Cache\Exception If the cache server doesn't respond
191
	 */
192
	public function getMultiple( iterable $keys, $default = null ) : iterable
193
	{
194
		try
195
		{
196
			$pos = 2;
197
			$list = [];
198
199
			$sql = $this->sql( 'get' );
200
			$sql = substr_replace( $sql, str_repeat( ',?', count( $keys) - 1 ), strrpos( $sql, '?' ) + 1, 0 );
201
202
			$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

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