1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan;
4:
5: use PHPStan\Type\BooleanType;
6: use PHPStan\Type\Constant\ConstantBooleanType;
7: use function array_column;
8: use function max;
9: use function min;
10:
11: /**
12: * @api
13: * @final
14: * @see https://phpstan.org/developing-extensions/trinary-logic
15: */
16: class TrinaryLogic
17: {
18:
19: private const YES = 1;
20: private const MAYBE = 0;
21: private const NO = -1;
22:
23: /** @var self[] */
24: private static array $registry = [];
25:
26: private function __construct(private int $value)
27: {
28: }
29:
30: public static function createYes(): self
31: {
32: return self::$registry[self::YES] ??= new self(self::YES);
33: }
34:
35: public static function createNo(): self
36: {
37: return self::$registry[self::NO] ??= new self(self::NO);
38: }
39:
40: public static function createMaybe(): self
41: {
42: return self::$registry[self::MAYBE] ??= new self(self::MAYBE);
43: }
44:
45: public static function createFromBoolean(bool $value): self
46: {
47: $yesNo = $value ? self::YES : self::NO;
48: return self::$registry[$yesNo] ??= new self($yesNo);
49: }
50:
51: private static function create(int $value): self
52: {
53: self::$registry[$value] ??= new self($value);
54: return self::$registry[$value];
55: }
56:
57: public function yes(): bool
58: {
59: return $this->value === self::YES;
60: }
61:
62: public function maybe(): bool
63: {
64: return $this->value === self::MAYBE;
65: }
66:
67: public function no(): bool
68: {
69: return $this->value === self::NO;
70: }
71:
72: public function toBooleanType(): BooleanType
73: {
74: if ($this->value === self::MAYBE) {
75: return new BooleanType();
76: }
77:
78: return new ConstantBooleanType($this->value === self::YES);
79: }
80:
81: public function and(self ...$operands): self
82: {
83: $operandValues = array_column($operands, 'value');
84: $operandValues[] = $this->value;
85: return self::create(min($operandValues));
86: }
87:
88: /**
89: * @template T
90: * @param T[] $objects
91: * @param callable(T): self $callback
92: */
93: public function lazyAnd(
94: array $objects,
95: callable $callback,
96: ): self
97: {
98: if ($this->no()) {
99: return $this;
100: }
101:
102: $results = [];
103: foreach ($objects as $object) {
104: $result = $callback($object);
105: if ($result->no()) {
106: return $result;
107: }
108:
109: $results[] = $result;
110: }
111:
112: return $this->and(...$results);
113: }
114:
115: public function or(self ...$operands): self
116: {
117: $operandValues = array_column($operands, 'value');
118: $operandValues[] = $this->value;
119: return self::create(max($operandValues));
120: }
121:
122: /**
123: * @template T
124: * @param T[] $objects
125: * @param callable(T): self $callback
126: */
127: public function lazyOr(
128: array $objects,
129: callable $callback,
130: ): self
131: {
132: if ($this->yes()) {
133: return $this;
134: }
135:
136: $results = [];
137: foreach ($objects as $object) {
138: $result = $callback($object);
139: if ($result->yes()) {
140: return $result;
141: }
142:
143: $results[] = $result;
144: }
145:
146: return $this->or(...$results);
147: }
148:
149: public static function extremeIdentity(self ...$operands): self
150: {
151: if ($operands === []) {
152: throw new ShouldNotHappenException();
153: }
154: $operandValues = array_column($operands, 'value');
155: $min = min($operandValues);
156: $max = max($operandValues);
157: return self::create($min === $max ? $min : self::MAYBE);
158: }
159:
160: /**
161: * @template T
162: * @param T[] $objects
163: * @param callable(T): self $callback
164: */
165: public static function lazyExtremeIdentity(
166: array $objects,
167: callable $callback,
168: ): self
169: {
170: if ($objects === []) {
171: throw new ShouldNotHappenException();
172: }
173:
174: $lastResult = null;
175: foreach ($objects as $object) {
176: $result = $callback($object);
177: if ($lastResult === null) {
178: $lastResult = $result;
179: continue;
180: }
181: if ($lastResult->equals($result)) {
182: continue;
183: }
184:
185: return self::createMaybe();
186: }
187:
188: return $lastResult;
189: }
190:
191: public static function maxMin(self ...$operands): self
192: {
193: if ($operands === []) {
194: throw new ShouldNotHappenException();
195: }
196: $operandValues = array_column($operands, 'value');
197: return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
198: }
199:
200: /**
201: * @template T
202: * @param T[] $objects
203: * @param callable(T): self $callback
204: */
205: public static function lazyMaxMin(
206: array $objects,
207: callable $callback,
208: ): self
209: {
210: $results = [];
211: foreach ($objects as $object) {
212: $result = $callback($object);
213: if ($result->yes()) {
214: return $result;
215: }
216:
217: $results[] = $result;
218: }
219:
220: return self::maxMin(...$results);
221: }
222:
223: public function negate(): self
224: {
225: return self::create(-$this->value);
226: }
227:
228: public function equals(self $other): bool
229: {
230: return $this === $other;
231: }
232:
233: public function compareTo(self $other): ?self
234: {
235: if ($this->value > $other->value) {
236: return $this;
237: } elseif ($other->value > $this->value) {
238: return $other;
239: }
240:
241: return null;
242: }
243:
244: public function describe(): string
245: {
246: static $labels = [
247: self::NO => 'No',
248: self::MAYBE => 'Maybe',
249: self::YES => 'Yes',
250: ];
251:
252: return $labels[$this->value];
253: }
254:
255: /**
256: * @param mixed[] $properties
257: */
258: public static function __set_state(array $properties): self
259: {
260: return self::create($properties['value']);
261: }
262:
263: }
264: