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