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: final 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: $min = $this->value;
83: foreach ($operands as $operand) {
84: if ($operand->value >= $min) {
85: continue;
86: }
87:
88: $min = $operand->value;
89: }
90: return self::create($min);
91: }
92:
93: /**
94: * @template T
95: * @param T[] $objects
96: * @param callable(T): self $callback
97: */
98: public function lazyAnd(
99: array $objects,
100: callable $callback,
101: ): self
102: {
103: if ($this->no()) {
104: return $this;
105: }
106:
107: $results = [];
108: foreach ($objects as $object) {
109: $result = $callback($object);
110: if ($result->no()) {
111: return $result;
112: }
113:
114: $results[] = $result;
115: }
116:
117: return $this->and(...$results);
118: }
119:
120: public function or(self ...$operands): self
121: {
122: $operandValues = array_column($operands, 'value');
123: $operandValues[] = $this->value;
124: return self::create(max($operandValues));
125: }
126:
127: /**
128: * @template T
129: * @param T[] $objects
130: * @param callable(T): self $callback
131: */
132: public function lazyOr(
133: array $objects,
134: callable $callback,
135: ): self
136: {
137: if ($this->yes()) {
138: return $this;
139: }
140:
141: $results = [];
142: foreach ($objects as $object) {
143: $result = $callback($object);
144: if ($result->yes()) {
145: return $result;
146: }
147:
148: $results[] = $result;
149: }
150:
151: return $this->or(...$results);
152: }
153:
154: public static function extremeIdentity(self ...$operands): self
155: {
156: if ($operands === []) {
157: throw new ShouldNotHappenException();
158: }
159: $operandValues = array_column($operands, 'value');
160: $min = min($operandValues);
161: $max = max($operandValues);
162: return self::create($min === $max ? $min : self::MAYBE);
163: }
164:
165: /**
166: * @template T
167: * @param T[] $objects
168: * @param callable(T): self $callback
169: */
170: public static function lazyExtremeIdentity(
171: array $objects,
172: callable $callback,
173: ): self
174: {
175: if ($objects === []) {
176: throw new ShouldNotHappenException();
177: }
178:
179: $lastResult = null;
180: foreach ($objects as $object) {
181: $result = $callback($object);
182: if ($lastResult === null) {
183: $lastResult = $result;
184: continue;
185: }
186: if ($lastResult->equals($result)) {
187: continue;
188: }
189:
190: return self::createMaybe();
191: }
192:
193: return $lastResult;
194: }
195:
196: public static function maxMin(self ...$operands): self
197: {
198: if ($operands === []) {
199: throw new ShouldNotHappenException();
200: }
201: $operandValues = array_column($operands, 'value');
202: return self::create(max($operandValues) > 0 ? 1 : min($operandValues));
203: }
204:
205: /**
206: * @template T
207: * @param T[] $objects
208: * @param callable(T): self $callback
209: */
210: public static function lazyMaxMin(
211: array $objects,
212: callable $callback,
213: ): self
214: {
215: $results = [];
216: foreach ($objects as $object) {
217: $result = $callback($object);
218: if ($result->yes()) {
219: return $result;
220: }
221:
222: $results[] = $result;
223: }
224:
225: return self::maxMin(...$results);
226: }
227:
228: public function negate(): self
229: {
230: return self::create(-$this->value);
231: }
232:
233: public function equals(self $other): bool
234: {
235: return $this === $other;
236: }
237:
238: public function compareTo(self $other): ?self
239: {
240: if ($this->value > $other->value) {
241: return $this;
242: } elseif ($other->value > $this->value) {
243: return $other;
244: }
245:
246: return null;
247: }
248:
249: public function describe(): string
250: {
251: static $labels = [
252: self::NO => 'No',
253: self::MAYBE => 'Maybe',
254: self::YES => 'Yes',
255: ];
256:
257: return $labels[$this->value];
258: }
259:
260: }
261: