Passed
Push — master ( 4a159f...7bc452 )
by Aimeos
03:58
created

Base::addMetaItemSingle()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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