1: <?php
2: declare(strict_types=1);
3:
4: namespace om\ICal;
5:
6: use InvalidArgumentException;
7: use Stringable;
8:
9: /**
10: * An immutable property: name, parameters and the raw (still escaped) value.
11: *
12: * Unknown and X- properties are kept like standard ones. Typed values are provided
13: * by om\ICal\Value\ValueParser and by the typed components (Event, Todo, ...).
14: */
15: final class Property implements Stringable {
16: /** Parameters are parsed on first access. */
17: public Parameters $parameters {
18: get => $this->parsed ??= Parameters::parse($this->rawParameters);
19: }
20:
21: private ?Parameters $parsed;
22: private readonly string $rawParameters;
23:
24: /**
25: * @param string|Parameters $parameters raw parameters (without the leading semicolon) or parsed ones
26: * @param string $value raw value as in the content line
27: * @param int $line number of the content line, 0 for created properties
28: */
29: public function __construct(
30: public readonly string $name,
31: string|Parameters $parameters,
32: public readonly string $value,
33: public readonly int $line = 0,
34: ) {
35: $this->rawParameters = is_string($parameters) ? $parameters : (string) $parameters;
36: $this->parsed = $parameters instanceof Parameters ? $parameters : null;
37: }
38:
39: /**
40: * @param array<string, string|list<string>>|Parameters $parameters
41: */
42: /**
43: * @param string $value the raw value; newlines are written as "\n" (use Text::escape() for TEXT)
44: * @param array<string, string|list<string>>|Parameters $parameters
45: * @throws InvalidArgumentException for an invalid property name
46: */
47: public static function create(string $name, string $value, array|Parameters $parameters = []): self {
48: if (!preg_match('/^[A-Za-z0-9-]+$/D', $name)) {
49: throw new InvalidArgumentException("Invalid property name: $name");
50: }
51: return new self(strtoupper($name), is_array($parameters) ? Parameters::from($parameters) : $parameters, $value);
52: }
53:
54: public static function fromContentLine(ContentLine $line): self {
55: return new self($line->name, $line->rawParameters, $line->value, $line->line);
56: }
57:
58: public function parameter(string $name): ?string {
59: if ($this->parsed === null && ($this->rawParameters === '' || stripos($this->rawParameters, $name) === false)) {
60: return null; // no need to parse the parameters
61: }
62: return $this->parameters->get($name);
63: }
64:
65: public function withValue(string $value): self {
66: return new self($this->name, $this->parameters, $value, $this->line);
67: }
68:
69: /**
70: * @param string|list<string>|null $value null removes the parameter
71: */
72: public function withParameter(string $name, string|array|null $value): self {
73: return new self($this->name, $this->parameters->with($name, $value), $this->value, $this->line);
74: }
75:
76: /**
77: * The unfolded content line, e.g. "DTSTART;TZID=Europe/Prague:20261010T100000".
78: */
79: /**
80: * The unfolded content line; newlines of the value are written as "\n", so they cannot start
81: * another content line.
82: */
83: public function __toString(): string {
84: $value = str_contains($this->value, "\n") || str_contains($this->value, "\r")
85: ? str_replace(["\r\n", "\n", "\r"], '\\n', $this->value)
86: : $this->value;
87: return $this->name . ($this->rawParameters === '' ? '' : ';' . $this->rawParameters) . ':' . $value;
88: }
89: }
90: