1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18namespace TYPO3\CMS\Core\DataHandling\Model; 19 20use TYPO3\CMS\Core\Utility\GeneralUtility; 21 22/** 23 * CorrelationId representation 24 * 25 * @todo Check internal state during v10 development 26 * @internal 27 */ 28class CorrelationId implements \JsonSerializable 29{ 30 protected const DEFAULT_VERSION = 1; 31 protected const PATTERN_V1 = '#^(?P<flags>[[:xdigit:]]{4})\$(?:(?P<scope>[[:alnum:]]+):)?(?P<subject>[[:alnum:]]+)(?P<aspects>(?:\/[[:alnum:]._-]+)*)$#'; 32 33 /** 34 * @var int 35 */ 36 protected $version = self::DEFAULT_VERSION; 37 38 /** 39 * @var string 40 */ 41 protected $scope; 42 43 /** 44 * @var int 45 */ 46 protected $capabilities = 0; 47 48 /** 49 * @var string 50 */ 51 protected $subject; 52 53 /** 54 * @var string[] 55 */ 56 protected $aspects = []; 57 58 /** 59 * @param string $scope 60 * @return static 61 */ 62 public static function forScope(string $scope): self 63 { 64 $target = static::create(); 65 $target->scope = $scope; 66 return $target; 67 } 68 69 public static function forSubject(string $subject, string ...$aspects): self 70 { 71 return static::create() 72 ->withSubject($subject) 73 ->withAspects(...$aspects); 74 } 75 76 /** 77 * @param string $correlationId 78 * @return static 79 */ 80 public static function fromString(string $correlationId): self 81 { 82 if (!preg_match(self::PATTERN_V1, $correlationId, $matches, PREG_UNMATCHED_AS_NULL)) { 83 throw new \InvalidArgumentException('Unknown format', 1569620858); 84 } 85 86 $flags = hexdec($matches['flags'] ?? 0); 87 $aspects = !empty($matches['aspects']) ? explode('/', ltrim($matches['aspects'] ?? '', '/')) : []; 88 $target = static::create() 89 ->withSubject($matches['subject']) 90 ->withAspects(...$aspects); 91 $target->scope = $matches['scope'] ?? null; 92 $target->version = $flags >> 10; 93 $target->capabilities = $flags & ((1 << 10) - 1); 94 return $target; 95 } 96 97 /** 98 * @return static 99 */ 100 protected static function create(): self 101 { 102 return GeneralUtility::makeInstance(static::class); 103 } 104 105 public function __toString(): string 106 { 107 if ($this->subject === null) { 108 throw new \LogicException('Cannot serialize for empty subject', 1569668681); 109 } 110 return $this->serialize(); 111 } 112 113 public function jsonSerialize(): string 114 { 115 return (string)$this; 116 } 117 118 public function withSubject(string $subject): self 119 { 120 if ($this->subject === $subject) { 121 return $this; 122 } 123 $target = clone $this; 124 $target->subject = $subject; 125 return $target; 126 } 127 128 public function withAspects(string ...$aspects): self 129 { 130 if ($this->aspects === $aspects) { 131 return $this; 132 } 133 $target = clone $this; 134 $target->aspects = $aspects; 135 return $target; 136 } 137 138 /** 139 * @return string|null 140 */ 141 public function getScope(): ?string 142 { 143 return $this->scope; 144 } 145 146 /** 147 * @return string|null 148 */ 149 public function getSubject(): ?string 150 { 151 return $this->subject; 152 } 153 154 /** 155 * @return string[] 156 */ 157 public function getAspects(): array 158 { 159 return $this->aspects; 160 } 161 162 /** 163 * v1 specs (eBNF) 164 * + FLAGS "$" [ SCOPE ":" ] SUBJECT { "/" ASPECT } 165 * + FLAGS ::= XDIGIT (* 16-bit integer big-endian) 166 * + SCOPE ::= ALNUM { ALNUM } 167 * + SUBJECT ::= ALNUM { ALNUM } 168 * + ASPECT ::= ( ALNUM | '.' | '_' | '-' ) { ( ALNUM | '.' | '_' | '-' ) } 169 */ 170 protected function serialize(): string 171 { 172 // 6-bit version 10-bit capabilities 173 $flags = $this->version << 10 + $this->capabilities; 174 return sprintf( 175 '%s$%s%s%s', 176 bin2hex(pack('n', $flags)), 177 $this->scope ? $this->scope . ':' : '', 178 $this->subject, 179 $this->aspects ? '/' . implode('/', $this->aspects) : '' 180 ); 181 } 182} 183