Passed
Push — master ( fd781a...b1eaa4 )
by Aimeos
08:26
created

Base::clientType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
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\Context\Item\Iface $context Context object
39
	 */
40
	public function __construct( \Aimeos\MShop\Context\Item\Iface $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
		$context = $this->context();
0 ignored issues
show
Unused Code introduced by
The assignment to $context is dead and can be removed.
Loading history...
57
58
		$view = $this->cachedView = $this->cachedView ?? $this->object()->data( $this->view() );
59
60
		foreach( $this->getSubClients() as $subclient ) {
61
			$html .= $subclient->setView( $view )->body( $uid );
62
		}
63
64
		return $view->set( 'body', $html )
65
			->render( $view->config( 'client/html/${type}/template-body', $type . '/body' ) );
66
	}
67
68
69
	/**
70
	 * Adds the data to the view object required by the templates
71
	 *
72
	 * @param \Aimeos\MW\View\Iface $view The view object which generates the HTML output
73
	 * @param array &$tags Result array for the list of tags that are associated to the output
74
	 * @param string|null &$expire Result variable for the expiration date of the output (null for no expiry)
75
	 * @return \Aimeos\MW\View\Iface The view object with the data required by the templates
76
	 */
77
	public function data( \Aimeos\MW\View\Iface $view, array &$tags = [], string &$expire = null ) : \Aimeos\MW\View\Iface
78
	{
79
		foreach( $this->getSubClients() as $name => $subclient ) {
80
			$view = $subclient->data( $view, $tags, $expire );
81
		}
82
83
		return $view;
84
	}
85
86
87
	/**
88
	 * Returns the sub-client given by its name.
89
	 *
90
	 * @param string $type Name of the client type
91
	 * @param string|null $name Name of the sub-client (Default if null)
92
	 * @return \Aimeos\Client\Html\Iface Sub-client object
93
	 */
94
	public function getSubClient( string $type, string $name = null ) : \Aimeos\Client\Html\Iface
95
	{
96
		return $this->createSubClient( $this->clientType() . '/' . $type, $name );
97
	}
98
99
100
	/**
101
	 * Returns the HTML string for insertion into the header.
102
	 *
103
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
104
	 * @return string|null String including HTML tags for the header on error
105
	 */
106
	public function header( string $uid = '' ) : ?string
107
	{
108
		$type = $this->clientType();
109
		$view = $this->cachedView = $this->cachedView ?? $this->object()->data( $this->view() );
110
111
		return $view->render( $view->config( 'client/html/${type}/template-header', $type . '/header' ) );
112
	}
113
114
115
	/**
116
	 * Processes the input, e.g. store given values.
117
	 *
118
	 * A view must be available and this method doesn't generate any output
119
	 * besides setting view variables.
120
	 */
121
	public function init()
122
	{
123
		$view = $this->view();
124
125
		foreach( $this->getSubClients() as $subclient ) {
126
			$subclient->setView( $view )->init();
127
		}
128
	}
129
130
131
	/**
132
	 * Modifies the cached content to replace content based on sessions or cookies.
133
	 *
134
	 * @param string $content Cached content
135
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
136
	 * @return string Modified content
137
	 */
138
	public function modify( string $content, string $uid ) : string
139
	{
140
		$view = $this->view();
141
142
		foreach( $this->getSubClients() as $subclient )
143
		{
144
			$subclient->setView( $view );
145
			$content = $subclient->modify( $content, $uid );
146
		}
147
148
		return $content;
149
	}
150
151
152
	/**
153
	 * Returns the PSR-7 response object for the request
154
	 *
155
	 * @return \Psr\Http\Message\ResponseInterface Response object
156
	 */
157
	public function response() : \Psr\Http\Message\ResponseInterface
158
	{
159
		return $this->view()->response();
160
	}
161
162
163
	/**
164
	 * Injects the reference of the outmost client object or decorator
165
	 *
166
	 * @param \Aimeos\Client\Html\Iface $object Reference to the outmost client or decorator
167
	 * @return \Aimeos\Client\Html\Iface Client object for chaining method calls
168
	 */
169
	public function setObject( \Aimeos\Client\Html\Iface $object ) : \Aimeos\Client\Html\Iface
170
	{
171
		$this->object = $object;
172
		return $this;
173
	}
174
175
176
	/**
177
	 * Sets the view object that will generate the HTML output.
178
	 *
179
	 * @param \Aimeos\MW\View\Iface $view The view object which generates the HTML output
180
	 * @return \Aimeos\Client\Html\Iface Reference to this object for fluent calls
181
	 */
182
	public function setView( \Aimeos\MW\View\Iface $view ) : \Aimeos\Client\Html\Iface
183
	{
184
		$this->view = $view;
185
		return $this;
186
	}
187
188
189
	/**
190
	 * Returns the outmost decorator of the decorator stack
191
	 *
192
	 * @return \Aimeos\Client\Html\Iface Outmost decorator object
193
	 */
194
	protected function object() : \Aimeos\Client\Html\Iface
195
	{
196
		if( $this->object !== null ) {
197
			return $this->object;
198
		}
199
200
		return $this;
201
	}
202
203
204
	/**
205
	 * Returns the view object that will generate the HTML output.
206
	 *
207
	 * @return \Aimeos\MW\View\Iface $view The view object which generates the HTML output
208
	 */
209
	protected function view() : \Aimeos\MW\View\Iface
210
	{
211
		if( !isset( $this->view ) ) {
212
			throw new \Aimeos\Client\Html\Exception( sprintf( 'No view available' ) );
213
		}
214
215
		return $this->view;
216
	}
217
218
219
	/**
220
	 * Adds the decorators to the client object
221
	 *
222
	 * @param \Aimeos\Client\Html\Iface $client Client object
223
	 * @param array $decorators List of decorator name that should be wrapped around the client
224
	 * @param string $classprefix Decorator class prefix, e.g. "\Aimeos\Client\Html\Catalog\Decorator\"
225
	 * @return \Aimeos\Client\Html\Iface Client object
226
	 */
227
	protected function addDecorators( \Aimeos\Client\Html\Iface $client, array $decorators, string $classprefix ) : \Aimeos\Client\Html\Iface
228
	{
229
		foreach( $decorators as $name )
230
		{
231
			if( ctype_alnum( $name ) === false )
232
			{
233
				$classname = is_string( $name ) ? $classprefix . $name : '<not a string>';
234
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid class name "%1$s"', $classname ) );
235
			}
236
237
			$classname = $classprefix . $name;
238
239
			if( class_exists( $classname ) === false ) {
240
				throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not found', $classname ) );
241
			}
242
243
			$client = new $classname( $client, $this->context );
244
245
			\Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Common\\Decorator\\Iface', $client );
246
		}
247
248
		return $client;
249
	}
250
251
252
	/**
253
	 * Adds the decorators to the client object
254
	 *
255
	 * @param \Aimeos\Client\Html\Iface $client Client object
256
	 * @param string $path Client string in lower case, e.g. "catalog/detail/basic"
257
	 * @return \Aimeos\Client\Html\Iface Client object
258
	 */
259
	protected function addClientDecorators( \Aimeos\Client\Html\Iface $client, string $path ) : \Aimeos\Client\Html\Iface
260
	{
261
		if( !is_string( $path ) || $path === '' ) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
262
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid domain "%1$s"', $path ) );
263
		}
264
265
		$localClass = str_replace( '/', '\\', ucwords( $path, '/' ) );
266
		$config = $this->context->config();
267
268
		$classprefix = '\\Aimeos\\Client\\Html\\Common\\Decorator\\';
269
		$decorators = $config->get( 'client/html/' . $path . '/decorators/global', [] );
270
		$client = $this->addDecorators( $client, $decorators, $classprefix );
271
272
		$classprefix = '\\Aimeos\\Client\\Html\\' . $localClass . '\\Decorator\\';
273
		$decorators = $config->get( 'client/html/' . $path . '/decorators/local', [] );
274
		$client = $this->addDecorators( $client, $decorators, $classprefix );
275
276
		return $client;
277
	}
278
279
280
	/**
281
	 * Adds the cache tags to the given list and sets a new expiration date if necessary based on the given item.
282
	 *
283
	 * @param array|\Aimeos\MShop\Common\Item\Iface $items Item or list of items, maybe with associated list items
284
	 * @param string|null &$expire Expiration date that will be overwritten if an earlier date is found
285
	 * @param array &$tags List of tags the new tags will be added to
286
	 * @param array $custom List of custom tags which are added too
287
	 */
288
	protected function addMetaItems( $items, string &$expire = null, array &$tags, array $custom = [] )
289
	{
290
		/** client/html/common/cache/tag-all
291
		 * Adds tags for all items used in a cache entry
292
		 *
293
		 * Each cache entry storing rendered parts for the HTML header or body
294
		 * can be tagged with information which items like texts, media, etc.
295
		 * are used in the HTML. This allows removing only those cache entries
296
		 * whose content has really changed and only that entries have to be
297
		 * rebuild the next time.
298
		 *
299
		 * The standard behavior stores only tags for each used domain, e.g. if
300
		 * a text is used, only the tag "text" is added. If you change a text
301
		 * in the administration interface, all cache entries with the tag
302
		 * "text" will be removed from the cache. This effectively wipes out
303
		 * almost all cached entries, which have to be rebuild with the next
304
		 * request.
305
		 *
306
		 * Important: As a list or detail view can use several hundred items,
307
		 * this configuration option will also add this number of tags to the
308
		 * cache entry. When using a cache adapter that can't insert all tags
309
		 * at once, this slows down the initial cache insert (and therefore the
310
		 * page speed) drastically! It's only recommended to enable this option
311
		 * if you use the DB, Mysql or Redis adapter that can insert all tags
312
		 * at once.
313
		 *
314
		 * @param boolean True to add tags for all items, false to use only a domain tag
315
		 * @since 2014.07
316
		 * @category Developer
317
		 * @category User
318
		 * @see client/html/common/cache/force
319
		 * @see madmin/cache/manager/name
320
		 * @see madmin/cache/name
321
		 */
322
		$tagAll = $this->context->config()->get( 'client/html/common/cache/tag-all', false );
323
324
		if( !is_array( $items ) && !is_map( $items ) ) {
325
			$items = map( [$items] );
326
		}
327
328
		$expires = $idMap = [];
329
330
		foreach( $items as $item )
331
		{
332
			if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface )
333
			{
334
				$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
335
				$idMap[$item->getResourceType()][] = $item->getId();
336
			}
337
338
			$this->addMetaItemSingle( $item, $expires, $tags, $tagAll );
339
		}
340
341
		if( $expire !== null ) {
342
			$expires[] = $expire;
343
		}
344
345
		if( !empty( $expires ) ) {
346
			$expire = min( $expires );
347
		}
348
349
		$tags = array_unique( array_merge( $tags, $custom ) );
350
	}
351
352
353
	/**
354
	 * Adds expire date and tags for a single item.
355
	 *
356
	 * @param \Aimeos\MShop\Common\Item\Iface $item Item, maybe with associated list items
357
	 * @param array &$expires Will contain the list of expiration dates
358
	 * @param array &$tags List of tags the new tags will be added to
359
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
360
	 */
361
	private function addMetaItemSingle( \Aimeos\MShop\Common\Item\Iface $item, array &$expires, array &$tags, bool $tagAll )
362
	{
363
		$domain = str_replace( '/', '_', $item->getResourceType() ); // maximum compatiblity
364
365
		if( $tagAll === true ) {
366
			$tags[] = $domain . '-' . $item->getId();
367
		} else {
368
			$tags[] = $domain;
369
		}
370
371
		if( $item instanceof \Aimeos\MShop\Common\Item\Time\Iface && ( $date = $item->getDateEnd() ) !== null ) {
372
			$expires[] = $date;
373
		}
374
375
		if( $item instanceof \Aimeos\MShop\Common\Item\ListsRef\Iface ) {
376
			$this->addMetaItemRef( $item, $expires, $tags, $tagAll );
377
		}
378
	}
379
380
381
	/**
382
	 * Adds expire date and tags for referenced items
383
	 *
384
	 * @param \Aimeos\MShop\Common\Item\ListsRef\Iface $item Item with associated list items
385
	 * @param array &$expires Will contain the list of expiration dates
386
	 * @param array &$tags List of tags the new tags will be added to
387
	 * @param bool $tagAll True of tags for all items should be added, false if only for the main item
388
	 */
389
	private function addMetaItemRef( \Aimeos\MShop\Common\Item\ListsRef\Iface $item, array &$expires, array &$tags, bool $tagAll )
390
	{
391
		foreach( $item->getListItems() as $listitem )
392
		{
393
			if( ( $refItem = $listitem->getRefItem() ) === null ) {
394
				continue;
395
			}
396
397
			if( $tagAll === true ) {
398
				$tags[] = str_replace( '/', '_', $listitem->getDomain() ) . '-' . $listitem->getRefId();
399
			}
400
401
			if( ( $date = $listitem->getDateEnd() ) !== null ) {
402
				$expires[] = $date;
403
			}
404
405
			$this->addMetaItemSingle( $refItem, $expires, $tags, $tagAll );
406
		}
407
	}
408
409
410
	/**
411
	 * Returns the client type of the class
412
	 *
413
	 * @return Client type, e.g. "catalog/detail"
0 ignored issues
show
Bug introduced by
The type Aimeos\Client\Html\Client was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
414
	 */
415
	protected function clientType() : string
416
	{
417
		return strtolower( trim( dirname( str_replace( '\\', '/', substr( get_class( $this ), 19 ) ) ), '/' ) );
418
	}
419
420
421
	/**
422
	 * Returns the sub-client given by its name.
423
	 *
424
	 * @param string $path Name of the sub-part in lower case (can contain a path like catalog/filter/tree)
425
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
426
	 * @return \Aimeos\Client\Html\Iface Sub-part object
427
	 */
428
	protected function createSubClient( string $path, string $name = null ) : \Aimeos\Client\Html\Iface
429
	{
430
		$path = strtolower( $path );
431
432
		if( $name === null ) {
433
			$name = $this->context->config()->get( 'client/html/' . $path . '/name', 'Standard' );
434
		}
435
436
		if( empty( $name ) || ctype_alnum( $name ) === false ) {
437
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Invalid characters in client name "%1$s"', $name ) );
438
		}
439
440
		$subnames = str_replace( '/', '\\', ucwords( $path, '/' ) );
441
		$classname = '\\Aimeos\\Client\\Html\\' . $subnames . '\\' . $name;
442
443
		if( class_exists( $classname ) === false ) {
444
			throw new \Aimeos\Client\Html\Exception( sprintf( 'Class "%1$s" not available', $classname ) );
445
		}
446
447
		$object = new $classname( $this->context );
448
		$object = \Aimeos\MW\Common\Base::checkClass( '\\Aimeos\\Client\\Html\\Iface', $object );
449
		$object = $this->addClientDecorators( $object, $path );
450
451
		return $object->setObject( $object );
452
	}
453
454
455
	/**
456
	 * Returns the minimal expiration date.
457
	 *
458
	 * @param string|null $first First expiration date or null
459
	 * @param string|null $second Second expiration date or null
460
	 * @return string|null Expiration date
461
	 */
462
	protected function expires( string $first = null, string $second = null ) : ?string
463
	{
464
		return ( $first !== null ? ( $second !== null ? min( $first, $second ) : $first ) : $second );
465
	}
466
467
468
	/**
469
	 * Returns the parameters used by the html client.
470
	 *
471
	 * @param array $params Associative list of all parameters
472
	 * @param array $prefixes List of prefixes the parameters must start with
473
	 * @return array Associative list of parameters used by the html client
474
	 */
475
	protected function getClientParams( array $params, array $prefixes = ['f_', 'l_', 'd_'] ) : array
476
	{
477
		return map( $params )->filter( function( $val, $key ) use ( $prefixes ) {
478
			return \Aimeos\MW\Str::starts( $key, $prefixes );
479
		} )->toArray();
480
	}
481
482
483
	/**
484
	 * Returns the context object.
485
	 *
486
	 * @return \Aimeos\MShop\Context\Item\Iface Context object
487
	 */
488
	protected function context() : \Aimeos\MShop\Context\Item\Iface
489
	{
490
		return $this->context;
491
	}
492
493
494
	/**
495
	 * Generates an unique hash from based on the input suitable to be used as part of the cache key
496
	 *
497
	 * @param array $prefixes List of prefixes the parameters must start with
498
	 * @param string $key Unique identifier if the content is placed more than once on the same page
499
	 * @param array $config Multi-dimensional array of configuration options used by the client and sub-clients
500
	 * @return string Unique hash
501
	 */
502
	protected function getParamHash( array $prefixes = ['f_', 'l_', 'd_'], string $key = '', array $config = [] ) : string
503
	{
504
		$locale = $this->context()->locale();
505
		$pstr = map( $this->getClientParams( $this->view()->param(), $prefixes ) )->ksort()->toJson();
506
507
		if( ( $cstr = json_encode( $config ) ) === false ) {
508
			throw new \Aimeos\Client\Html\Exception( 'Unable to encode parameters or configuration options' );
509
		}
510
511
		return md5( $key . $pstr . $cstr . $locale->getLanguageId() . $locale->getCurrencyId() . $locale->getSiteId() );
512
	}
513
514
515
	/**
516
	 * Returns the list of sub-client names configured for the client.
517
	 *
518
	 * @return array List of HTML client names
519
	 */
520
	protected function getSubClientNames() : array
521
	{
522
		return [];
523
	}
524
525
526
	/**
527
	 * Returns the configured sub-clients or the ones named in the default parameter if none are configured.
528
	 *
529
	 * @return array List of sub-clients implementing \Aimeos\Client\Html\Iface	ordered in the same way as the names
530
	 */
531
	protected function getSubClients() : array
532
	{
533
		if( !isset( $this->subclients ) )
534
		{
535
			$this->subclients = [];
536
537
			foreach( $this->getSubClientNames() as $name ) {
538
				$this->subclients[$name] = $this->getSubClient( $name );
539
			}
540
		}
541
542
		return $this->subclients;
543
	}
544
545
546
	/**
547
	 * Returns the template for the given configuration key
548
	 *
549
	 * If the "l_type" parameter is present, a specific template for this given
550
	 * type is used if available.
551
	 *
552
	 * @param string $confkey Key to the configuration setting for the template
553
	 * @param string $default Default template if none is configured or not found
554
	 * @return string Relative template path
555
	 */
556
	protected function getTemplatePath( string $confkey, string $default ) : string
557
	{
558
		if( ( $type = $this->view->param( 'l_type' ) ) !== null && ctype_alnum( $type ) !== false ) {
559
			return $this->view->config( $confkey . '-' . $type, $this->view->config( $confkey, $default ) );
560
		}
561
562
		return $this->view->config( $confkey, $default );
563
	}
564
565
566
	/**
567
	 * Returns the cache entry for the given unique ID and type.
568
	 *
569
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
570
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
571
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
572
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
573
	 * @return string|null Cached entry or null if not available
574
	 */
575
	protected function getCached( string $type, string $uid, array $prefixes, string $confkey ) : ?string
576
	{
577
		$context = $this->context();
578
		$config = $context->config();
579
580
		/** client/html/common/cache/force
581
		 * Enforces content caching regardless of user logins
582
		 *
583
		 * Caching the component output is normally disabled as soon as the
584
		 * user has logged in. This enables displaying user or user group
585
		 * specific content without mixing standard and user specific output.
586
		 *
587
		 * If you don't have any user or user group specific content
588
		 * (products, categories, attributes, media, prices, texts, etc.),
589
		 * you can enforce content caching nevertheless to keep response
590
		 * times as low as possible.
591
		 *
592
		 * @param boolean True to cache output regardless of login, false for no caching
593
		 * @since 2015.08
594
		 * @category Developer
595
		 * @category User
596
		 * @see client/html/common/cache/tag-all
597
		 */
598
		$force = $config->get( 'client/html/common/cache/force', false );
599
		$enable = $config->get( $confkey . '/cache', true );
600
601
		if( $enable == false || $force == false && $context->user() !== null ) {
602
			return null;
603
		}
604
605
		$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
606
607
		$keys = array(
608
			'body' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':body', $cfg ),
609
			'header' => $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':header', $cfg ),
610
		);
611
612
		if( !isset( $this->cache[$keys[$type]] ) ) {
613
			$this->cache = $context->cache()->getMultiple( $keys );
614
		}
615
616
		return ( isset( $this->cache[$keys[$type]] ) ? $this->cache[$keys[$type]] : null );
617
	}
618
619
620
	/**
621
	 * Returns the cache entry for the given type and unique ID.
622
	 *
623
	 * @param string $type Type of the cache entry, i.e. "body" or "header"
624
	 * @param string $uid Unique identifier for the output if the content is placed more than once on the same page
625
	 * @param string[] $prefixes List of prefixes of all parameters that are relevant for generating the output
626
	 * @param string $confkey Configuration key prefix that matches all relevant settings for the component
627
	 * @param string $value Value string that should be stored for the given key
628
	 * @param array $tags List of tag strings that should be assoicated to the given value in the cache
629
	 * @param string|null $expire Date/time string in "YYYY-MM-DD HH:mm:ss"	format when the cache entry expires
630
	 */
631
	protected function setCached( string $type, string $uid, array $prefixes, string $confkey, string $value, array $tags, string $expire = null )
632
	{
633
		$context = $this->context();
634
		$config = $context->config();
635
636
		$force = $config->get( 'client/html/common/cache/force', false );
637
		$enable = $config->get( $confkey . '/cache', true );
638
639
		if( $enable == false || $force == false && $context->user() !== null ) {
640
			return;
641
		}
642
643
		try
644
		{
645
			$cfg = array_merge( $config->get( 'client/html', [] ), $this->getSubClientNames() );
646
			$key = $this->getParamHash( $prefixes, $uid . ':' . $confkey . ':' . $type, $cfg );
647
648
			$context->cache()->set( $key, $value, $expire, array_unique( $tags ) );
649
		}
650
		catch( \Exception $e )
651
		{
652
			$msg = sprintf( 'Unable to set cache entry: %1$s', $e->getMessage() );
653
			$context->logger()->notice( $msg, 'client/html' );
654
		}
655
	}
656
657
658
	/**
659
	 * Writes the exception details to the log
660
	 *
661
	 * @param \Exception $e Exception object
662
	 */
663
	protected function logException( \Exception $e )
664
	{
665
		$msg = $e->getMessage() . PHP_EOL . $e->getTraceAsString();
666
		$this->context->logger()->warning( $msg, '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 + $mlen < $clen && ( $start = @strpos( $content, $marker, $start ) ) !== false )
686
		{
687
			if( ( $end = strpos( $content, $marker, $start + 1 ) ) !== false ) {
688
				$content = substr_replace( $content, $section, $start, $end - $start + $mlen );
689
			}
690
691
			$start += 2 * $mlen + $len;
692
		}
693
694
		return $content;
695
	}
696
}
697