Passed
Push — master ( bf78e6...b84acb )
by Aimeos
02:31
created

Base::addDecorators()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 5
nop 3
dl 0
loc 22
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @license LGPLv3, http://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2012
6
 * @copyright Aimeos (aimeos.org), 2015-2022
7
 * @package Client
8
 * @subpackage Html
9
 */
10
11
12
namespace Aimeos\Client\Html;
13
14
15
/**
16
 * Common abstract class for all HTML client classes.
17
 *
18
 * @package Client
19
 * @subpackage Html
20
 */
21
abstract class Base
22
	implements \Aimeos\Client\Html\Iface, \Aimeos\Macro\Iface
23
{
24
	use \Aimeos\Macro\Macroable;
25
26
27
	private $view;
28
	private $cache;
29
	private $object;
30
	private $context;
31
	private $subclients;
32
	private $cachedView;
33
34
35
	/**
36
	 * Initializes the class instance.
37
	 *
38
	 * @param \Aimeos\MShop\ContextIface $context Context object
39
	 */
40
	public function __construct( \Aimeos\MShop\ContextIface $context )
41
	{
42
		$this->context = $context;
43
	}
44
45
46
	/**
47
	 * Returns the HTML code for insertion into the body.
48
	 *
49
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
50
	 * @return string HTML code
51
	 */
52
	public function body( string $uid = '' ) : string
53
	{
54
		$html = '';
55
		$type = $this->clientType();
56
57
		$parts = explode( '/', $type );
58
		$list = array_merge( $parts, ['body'] );
59
60
		$template = join( '/', array_splice( $list, 0, 2, [] ) ) . '/' . join( '-', $list );
61
62
		// poplate view only for component, not for subparts
63
		if( count( $parts ) === 2 ) {
64
			$view = $this->cachedView = $this->cachedView ?? $this->object()->data( $this->view() );
65
		} else {
66
			$view = $this->view();
67
		}
68
69
		foreach( $this->getSubClients() as $subclient ) {
70
			$html .= $subclient->setView( $view )->body( $uid );
71
		}
72
73
		return $view->set( 'body', $html )
74
			->render( $view->config( 'client/html/${type}/template-body', $template ) );
75
	}
76
77
78
	/**
79
	 * Adds the data to the view object required by the templates
80
	 *
81
	 * @param \Aimeos\Base\View\Iface $view The view object which generates the HTML output
82
	 * @param array &$tags Result array for the list of tags that are associated to the output
83
	 * @param string|null &$expire Result variable for the expiration date of the output (null for no expiry)
84
	 * @return \Aimeos\Base\View\Iface The view object with the data required by the templates
85
	 */
86
	public function data( \Aimeos\Base\View\Iface $view, array &$tags = [], string &$expire = null ) : \Aimeos\Base\View\Iface
87
	{
88
		foreach( $this->getSubClients() as $name => $subclient ) {
89
			$view = $subclient->data( $view, $tags, $expire );
90
		}
91
92
		return $view;
93
	}
94
95
96
	/**
97
	 * Returns the sub-client given by its name.
98
	 *
99
	 * @param string $type Name of the client type
100
	 * @param string|null $name Name of the sub-client (Default if null)
101
	 * @return \Aimeos\Client\Html\Iface Sub-client object
102
	 */
103
	public function getSubClient( string $type, string $name = null ) : \Aimeos\Client\Html\Iface
104
	{
105
		return $this->createSubClient( $this->clientType() . '/' . $type, $name );
106
	}
107
108
109
	/**
110
	 * Returns the HTML string for insertion into the header.
111
	 *
112
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
113
	 * @return string|null String including HTML tags for the header on error
114
	 */
115
	public function header( string $uid = '' ) : ?string
116
	{
117
		$type = $this->clientType();
118
		$view = $this->cachedView = $this->cachedView ?? $this->object()->data( $this->view() );
119
120
		return $view->render( $view->config( 'client/html/${type}/template-header', $type . '/header' ) );
121
	}
122
123
124
	/**
125
	 * Processes the input, e.g. store given values.
126
	 *
127
	 * A view must be available and this method doesn't generate any output
128
	 * besides setting view variables.
129
	 */
130
	public function init()
131
	{
132
		$view = $this->view();
133
134
		foreach( $this->getSubClients() as $subclient ) {
135
			$subclient->setView( $view )->init();
136
		}
137
	}
138
139
140
	/**
141
	 * Modifies the cached content to replace content based on sessions or cookies.
142
	 *
143
	 * @param string $content Cached content
144
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
145
	 * @return string Modified content
146
	 */
147
	public function modify( string $content, string $uid ) : string
148
	{
149
		$view = $this->view();
150
151
		foreach( $this->getSubClients() as $subclient )
152
		{
153
			$subclient->setView( $view );
154
			$content = $subclient->modify( $content, $uid );
155
		}
156
157
		return $content;
158
	}
159
160
161
	/**
162
	 * Returns the PSR-7 response object for the request
163
	 *
164
	 * @return \Psr\Http\Message\ResponseInterface Response object
165
	 */
166
	public function response() : \Psr\Http\Message\ResponseInterface
167
	{
168
		return $this->view()->response();
169
	}
170
171
172
	/**
173
	 * Injects the reference of the outmost client object or decorator
174
	 *
175
	 * @param \Aimeos\Client\Html\Iface $object Reference to the outmost client or decorator
176
	 * @return \Aimeos\Client\Html\Iface Client object for chaining method calls
177
	 */
178
	public function setObject( \Aimeos\Client\Html\Iface $object ) : \Aimeos\Client\Html\Iface
179
	{
180
		$this->object = $object;
181
		return $this;
182
	}
183
184
185
	/**
186
	 * Sets the view object that will generate the HTML output.
187
	 *
188
	 * @param \Aimeos\Base\View\Iface $view The view object which generates the HTML output
189
	 * @return \Aimeos\Client\Html\Iface Reference to this object for fluent calls
190
	 */
191
	public function setView( \Aimeos\Base\View\Iface $view ) : \Aimeos\Client\Html\Iface
192
	{
193
		$this->view = $view;
194
		return $this;
195
	}
196
197
198
	/**
199
	 * Returns the outmost decorator of the decorator stack
200
	 *
201
	 * @return \Aimeos\Client\Html\Iface Outmost decorator object
202
	 */
203
	protected function object() : \Aimeos\Client\Html\Iface
204
	{
205
		if( $this->object !== null ) {
206
			return $this->object;
207
		}
208
209
		return $this;
210
	}
211
212
213
	/**
214
	 * Returns the view object that will generate the HTML output.
215
	 *
216
	 * @return \Aimeos\Base\View\Iface $view The view object which generates the HTML output
217
	 */
218
	protected function view() : \Aimeos\Base\View\Iface
219
	{
220
		if( !isset( $this->view ) ) {
221
			throw new \Aimeos\Client\Html\Exception( sprintf( 'No view available' ) );
222
		}
223
224
		return $this->view;
225
	}
226
227
228
	/**
229
	 * Adds the decorators to the client object
230
	 *
231
	 * @param \Aimeos\Client\Html\Iface $client Client object
232
	 * @param array $decorators List of decorator name that should be wrapped around the client
233
	 * @param string $classprefix Decorator class prefix, e.g. "\Aimeos\Client\Html\Catalog\Decorator\"
234
	 * @return \Aimeos\Client\Html\Iface Client object
235
	 */
236
	protected function addDecorators( \Aimeos\Client\Html\Iface $client, array $decorators, string $classprefix ) : \Aimeos\Client\Html\Iface
237
	{
238
		foreach( $decorators as $name )
239
		{
240
			if( ctype_alnum( $name ) === false )
241
			{
242
				$classname = is_string( $name ) ? $classprefix . $name : '<not a string>';
243
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid class name "%1$s"', $classname ) );
244
			}
245
246
			$classname = $classprefix . $name;
247
248
			if( class_exists( $classname ) === false ) {
249
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not found', $classname ) );
250
			}
251
252
			$client = new $classname( $client, $this->context );
253
254
			\Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Common\\Decorator\\Iface', $client );
255
		}
256
257
		return $client;
258
	}
259
260
261
	/**
262
	 * Adds the decorators to the client object
263
	 *
264
	 * @param \Aimeos\Client\Html\Iface $client Client object
265
	 * @param string $path Client string in lower case, e.g. "catalog/detail/basic"
266
	 * @return \Aimeos\Client\Html\Iface Client object
267
	 */
268
	protected function addClientDecorators( \Aimeos\Client\Html\Iface $client, string $path ) : \Aimeos\Client\Html\Iface
269
	{
270
		if( !is_string( $path ) || $path === '' ) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
271
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid domain "%1$s"', $path ) );
272
		}
273
274
		$localClass = str_replace( '/', '\\', ucwords( $path, '/' ) );
275
		$config = $this->context->config();
276
277
		$classprefix = '\\Aimeos\\Client\\Html\\Common\\Decorator\\';
278
		$decorators = $config->get( 'client/html/' . $path . '/decorators/global', [] );
279
		$client = $this->addDecorators( $client, $decorators, $classprefix );
280
281
		$classprefix = '\\Aimeos\\Client\\Html\\' . $localClass . '\\Decorator\\';
282
		$decorators = $config->get( 'client/html/' . $path . '/decorators/local', [] );
283
		$client = $this->addDecorators( $client, $decorators, $classprefix );
284
285
		return $client;
286
	}
287
288
289
	/**
290
	 * Adds the cache tags to the given list and sets a new expiration date if necessary based on the given item.
291
	 *
292
	 * @param \Aimeos\MShop\Common\Item\Iface|iterable $items Item or list of items, maybe with associated list items
293
	 * @param string|null &$expire Expiration date that will be overwritten if an earlier date is found
294
	 * @param array &$tags List of tags the new tags will be added to
295
	 * @param array $custom List of custom tags which are added too
296
	 */
297
	protected function addMetaItems( $items, string &$expire = null, array &$tags, array $custom = [] )
298
	{
299
		/** client/html/common/cache/tag-all
300
		 * Adds tags for all items used in a cache entry
301
		 *
302
		 * Each cache entry storing rendered parts for the HTML header or body
303
		 * can be tagged with information which items like texts, media, etc.
304
		 * are used in the HTML. This allows removing only those cache entries
305
		 * whose content has really changed and only that entries have to be
306
		 * rebuild the next time.
307
		 *
308
		 * The standard behavior stores only tags for each used domain, e.g. if
309
		 * a text is used, only the tag "text" is added. If you change a text
310
		 * in the administration interface, all cache entries with the tag
311
		 * "text" will be removed from the cache. This effectively wipes out
312
		 * almost all cached entries, which have to be rebuild with the next
313
		 * request.
314
		 *
315
		 * Important: As a list or detail view can use several hundred items,
316
		 * this configuration option will also add this number of tags to the
317
		 * cache entry. When using a cache adapter that can't insert all tags
318
		 * at once, this slows down the initial cache insert (and therefore the
319
		 * page speed) drastically! It's only recommended to enable this option
320
		 * if you use the DB, Mysql or Redis adapter that can insert all tags
321
		 * at once.
322
		 *
323
		 * @param boolean True to add tags for all items, false to use only a domain tag
324
		 * @since 2014.07
325
		 * @see client/html/common/cache/force
326
		 * @see madmin/cache/manager/name
327
		 * @see madmin/cache/name
328
		 */
329
		$tagAll = $this->context->config()->get( 'client/html/common/cache/tag-all', true );
330
331
		$expires = [];
332
333
		foreach( map( $items ) as $item )
334
		{
335
			if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) {
336
				$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
337
			}
338
339
			$this->addMetaItemSingle( $item, $expires, $tags, $tagAll );
340
		}
341
342
		if( $expire !== null ) {
343
			$expires[] = $expire;
344
		}
345
346
		if( !empty( $expires ) ) {
347
			$expire = min( $expires );
348
		}
349
350
		$tags = array_unique( array_merge( $tags, $custom ) );
351
352
		return $items;
353
	}
354
355
356
	/**
357
	 * Adds the cache tags to the given list and sets a new expiration date if necessary based on the given catalog tree.
358
	 *
359
	 * @param \Aimeos\MShop\Catalog\Item\Iface $tree Tree node, maybe with sub-nodes
360
	 * @param string|null &$expire Expiration date that will be overwritten if an earlier date is found
361
	 * @param array &$tags List of tags the new tags will be added to
362
	 * @param array $custom List of custom tags which are added too
363
	 */
364
	protected function addMetaItemCatalog( \Aimeos\MShop\Catalog\Item\Iface $tree, string &$expire = null, array &$tags = [], array $custom = [] )
365
	{
366
		$this->addMetaItems( $tree, $expire, $tags, $custom );
367
368
		foreach( $tree->getChildren() as $child ) {
369
			$this->addMetaItemCatalog( $child, $expire, $tags, $custom );
370
		}
371
	}
372
373
374
	/**
375
	 * Adds expire date and tags for a single item.
376
	 *
377
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item, maybe with associated list items
378
	 * @param array &$expires Will contain the list of expiration dates
379
	 * @param array &$tags List of tags the new tags will be added to
380
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
381
	 */
382
	private function addMetaItemSingle( \Aimeos\MShop\Common\Item\Iface $item, array &$expires, array &$tags, bool $tagAll )
383
	{
384
		$domain = str_replace( '/', '_', $item->getResourceType() ); // maximum compatiblity
385
386
		if( in_array( $item->getResourceType(), ['catalog', 'product', 'supplier'] ) ) {
387
			$tags[] = $tagAll ? $domain . '-' . $item->getId() : $domain ;
388
		}
389
390
		if( $item instanceof \Aimeos\MShop\Common\Item\Time\Iface && ( $date = $item->getDateEnd() ) !== null ) {
391
			$expires[] = $date;
392
		}
393
394
		if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) {
395
			$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
396
		}
397
	}
398
399
400
	/**
401
	 * Adds expire date and tags for referenced items
402
	 *
403
	 * @param \Aimeos\MShop\Common\Item\ListsRef\Iface $item Item with associated list items
404
	 * @param array &$expires Will contain the list of expiration dates
405
	 * @param array &$tags List of tags the new tags will be added to
406
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
407
	 */
408
	private function addMetaItemRef( \Aimeos\MShop\Common\Item\ListsRef\Iface $item, array &$expires, array &$tags, bool $tagAll )
409
	{
410
		foreach( $item->getListItems() as $listItem )
411
		{
412
			if( ( $refItem = $listItem->getRefItem() ) === null ) {
413
				continue;
414
			}
415
416
			if( $tagAll === true && in_array( $listItem->getDomain(), ['catalog', 'product', 'supplier'] ) ) {
417
				$tags[] = str_replace( '/', '_', $listItem->getDomain() ) . '-' . $listItem->getRefId();
418
			}
419
420
			if( ( $date = $listItem->getDateEnd() ) !== null ) {
421
				$expires[] = $date;
422
			}
423
424
			$this->addMetaItemSingle( $refItem, $expires, $tags, $tagAll );
425
		}
426
	}
427
428
429
	/**
430
	 * Returns the client type of the class
431
	 *
432
	 * @return string Client type, e.g. "catalog/detail"
433
	 */
434
	protected function clientType() : string
435
	{
436
		return strtolower( trim( dirname( str_replace( '\\', '/', substr( get_class( $this ), 19 ) ) ), '/' ) );
437
	}
438
439
440
	/**
441
	 * Returns the sub-client given by its name.
442
	 *
443
	 * @param string $path Name of the sub-part in lower case (can contain a path like catalog/filter/tree)
444
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
445
	 * @return \Aimeos\Client\Html\Iface Sub-part object
446
	 */
447
	protected function createSubClient( string $path, string $name = null ) : \Aimeos\Client\Html\Iface
448
	{
449
		$path = strtolower( $path );
450
451
		if( $name === null ) {
452
			$name = $this->context->config()->get( 'client/html/' . $path . '/name', 'Standard' );
453
		}
454
455
		if( empty( $name ) || ctype_alnum( $name ) === false ) {
456
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid characters in client name "%1$s"', $name ) );
457
		}
458
459
		$subnames = str_replace( '/', '\\', ucwords( $path, '/' ) );
460
		$classname = '\\Aimeos\\Client\\Html\\' . $subnames . '\\' . $name;
461
462
		if( class_exists( $classname ) === false ) {
463
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not available', $classname ) );
464
		}
465
466
		$object = new $classname( $this->context );
467
		$object = \Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Iface', $object );
468
		$object = $this->addClientDecorators( $object, $path );
469
470
		return $object->setObject( $object );
471
	}
472
473
474
	/**
475
	 * Returns the minimal expiration date.
476
	 *
477
	 * @param string|null $first First expiration date or null
478
	 * @param string|null $second Second expiration date or null
479
	 * @return string|null Expiration date
480
	 */
481
	protected function expires( string $first = null, string $second = null ) : ?string
482
	{
483
		return ( $first !== null ? ( $second !== null ? min( $first, $second ) : $first ) : $second );
484
	}
485
486
487
	/**
488
	 * Returns the parameters used by the html client.
489
	 *
490
	 * @param array $params Associative list of all parameters
491
	 * @param array $prefixes List of prefixes the parameters must start with
492
	 * @return array Associative list of parameters used by the html client
493
	 */
494
	protected function getClientParams( array $params, array $prefixes = ['f_', 'l_', 'd_'] ) : array
495
	{
496
		return map( $params )->filter( function( $val, $key ) use ( $prefixes ) {
497
			return \Aimeos\Base\Str::starts( $key, $prefixes );
498
		} )->toArray();
499
	}
500
501
502
	/**
503
	 * Returns the context object.
504
	 *
505
	 * @return \Aimeos\MShop\ContextIface Context object
506
	 */
507
	protected function context() : \Aimeos\MShop\ContextIface
508
	{
509
		return $this->context;
510
	}
511
512
513
	/**
514
	 * Generates an unique hash from based on the input suitable to be used as part of the cache key
515
	 *
516
	 * @param array $prefixes List of prefixes the parameters must start with
517
	 * @param string $key Unique identifier if the content is placed more than once on the same page
518
	 * @param array $config Multi-dimensional array of configuration options used by the client and sub-clients
519
	 * @return string Unique hash
520
	 */
521
	protected function getParamHash( array $prefixes = ['f_', 'l_', 'd_'], string $key = '', array $config = [] ) : string
522
	{
523
		$locale = $this->context()->locale();
524
		$pstr = map( $this->getClientParams( $this->view()->param(), $prefixes ) )->ksort()->toJson();
525
526
		if( ( $cstr = json_encode( $config ) ) === false ) {
527
			throw new \Aimeos\Client\Html\Exception( 'Unable to encode parameters or configuration options' );
528
		}
529
530
		return md5( $key . $pstr . $cstr . $locale->getLanguageId() . $locale->getCurrencyId() . $locale->getSiteId() );
531
	}
532
533
534
	/**
535
	 * Returns the list of sub-client names configured for the client.
536
	 *
537
	 * @return array List of HTML client names
538
	 */
539
	protected function getSubClientNames() : array
540
	{
541
		return [];
542
	}
543
544
545
	/**
546
	 * Returns the configured sub-clients or the ones named in the default parameter if none are configured.
547
	 *
548
	 * @return array List of sub-clients implementing \Aimeos\Client\Html\Iface	ordered in the same way as the names
549
	 */
550
	protected function getSubClients() : array
551
	{
552
		if( !isset( $this->subclients ) )
553
		{
554
			$this->subclients = [];
555
556
			foreach( $this->getSubClientNames() as $name ) {
557
				$this->subclients[$name] = $this->getSubClient( $name );
558
			}
559
		}
560
561
		return $this->subclients;
562
	}
563
564
565
	/**
566
	 * Returns the cache entry for the given unique ID and type.
567
	 *
568
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
569
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
570
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
571
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
572
	 * @return string|null Cached entry or null if not available
573
	 */
574
	protected function cached( string $type, string $uid, array $prefixes, string $confkey ) : ?string
575
	{
576
		$context = $this->context();
577
		$config = $context->config();
578
579
		/** client/html/common/cache/force
580
		 * Enforces content caching regardless of user logins
581
		 *
582
		 * Caching the component output is normally disabled as soon as the
583
		 * user has logged in. This enables displaying user or user group
584
		 * specific content without mixing standard and user specific output.
585
		 *
586
		 * If you don't have any user or user group specific content
587
		 * (products, categories, attributes, media, prices, texts, etc.),
588
		 * you can enforce content caching nevertheless to keep response
589
		 * times as low as possible.
590
		 *
591
		 * @param boolean True to cache output regardless of login, false for no caching
592
		 * @since 2015.08
593
		 * @see client/html/common/cache/tag-all
594
		 */
595
		$force = $config->get( 'client/html/common/cache/force', false );
596
		$enable = $config->get( $confkey . '/cache', true );
597
598
		if( $enable == false || $force == false && $context->user() !== null ) {
599
			return null;
600
		}
601
602
		$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
603
604
		$keys = array(
605
			'body' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':body', $cfg ),
606
			'header' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':header', $cfg ),
607
		);
608
609
		if( !isset( $this->cache[$keys[$type]] ) ) {
610
			$this->cache = $context->cache()->getMultiple( $keys );
611
		}
612
613
		return ( isset( $this->cache[$keys[$type]] ) ? $this->cache[$keys[$type]] : null );
614
	}
615
616
617
	/**
618
	 * Returns the cache entry for the given type and unique ID.
619
	 *
620
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
621
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
622
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
623
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
624
	 * @param string $value Value string that should be stored for the given key
625
	 * @param array $tags List of tag strings that should be assoicated to the given value in the cache
626
	 * @param string|null $expire Date/time string in "YYYY-MM-DD HH:mm:ss"	format when the cache entry expires
627
	 */
628
	protected function cache( string $type, string $uid, array $prefixes, string $confkey, string $value, array $tags, string $expire = null ) : string
629
	{
630
		$context = $this->context();
631
		$config = $context->config();
632
633
		$force = $config->get( 'client/html/common/cache/force', false );
634
		$enable = $config->get( $confkey . '/cache', true );
635
636
		if( !$value || !$enable || !$force && $context->user() ) {
637
			return $value;
638
		}
639
640
		try
641
		{
642
			$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
643
			$key = $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':' . $type, $cfg );
644
645
			$context->cache()->set( $key, $value, $expire, array_unique( $tags ) );
646
		}
647
		catch( \Exception $e )
648
		{
649
			$context->logger()->notice( sprintf( 'Unable to set cache entry: %1$s', $e->getMessage() ), 'client/html' );
650
		}
651
652
		return $value;
653
	}
654
655
656
	/**
657
	 * Writes the exception details to the log
658
	 *
659
	 * @param \Exception $e Exception object
660
	 * @param int $level Log level of the exception
661
	 */
662
	protected function logException( \Exception $e, int $level = \Aimeos\Base\Logger\Iface::WARN )
663
	{
664
		$uri = $this->view()->request()->getServerParams()['REQUEST_URI'] ?? '';
665
		$msg = ( $uri ? $uri . PHP_EOL : '' ) . $e->getMessage() . PHP_EOL . $e->getTraceAsString();
666
		$this->context->logger()->log( $msg, $level, 'client/html' );
667
	}
668
669
670
	/**
671
	 * Replaces the section in the content that is enclosed by the marker.
672
	 *
673
	 * @param string $content Cached content
674
	 * @param string $section New section content
675
	 * @param string $marker Name of the section marker without "<!-- " and " -->" parts
676
	 */
677
	protected function replaceSection( string $content, string $section, string $marker ) : string
678
	{
679
		$marker = '<!-- ' . $marker . ' -->';
680
		$clen = strlen( $content );
681
		$mlen = strlen( $marker );
682
		$len = strlen( $section );
683
		$start = 0;
684
685
		while( $start + 2 * $mlen <= $clen && ( $start = strpos( $content, $marker, $start ) ) !== false )
686
		{
687
			if( ( $end = strpos( $content, $marker, $start + $mlen ) ) !== false ) {
688
				$content = substr_replace( $content, $section, $start + $mlen, $end - $start - $mlen );
689
			}
690
691
			$start += 2 * $mlen + $len;
692
		}
693
694
		return $content;
695
	}
696
}
697