| 1: | <?php |
| 2: | |
| 3: | declare(strict_types=1); |
| 4: | |
| 5: | namespace Atk4\Data\Model\Scope; |
| 6: | |
| 7: | use Atk4\Core\ReadableCaptionTrait; |
| 8: | use Atk4\Data\Exception; |
| 9: | use Atk4\Data\Field; |
| 10: | use Atk4\Data\Model; |
| 11: | use Atk4\Data\Persistence; |
| 12: | use Atk4\Data\Persistence\Sql\Expression; |
| 13: | use Atk4\Data\Persistence\Sql\Expressionable; |
| 14: | use Atk4\Data\Persistence\Sql\Sqlite\Expression as SqliteExpression; |
| 15: | |
| 16: | class Condition extends AbstractScope |
| 17: | { |
| 18: | use ReadableCaptionTrait; |
| 19: | |
| 20: | |
| 21: | public $key; |
| 22: | |
| 23: | |
| 24: | public $operator; |
| 25: | |
| 26: | |
| 27: | public $value; |
| 28: | |
| 29: | public const OPERATOR_EQUALS = '='; |
| 30: | public const OPERATOR_DOESNOT_EQUAL = '!='; |
| 31: | public const OPERATOR_GREATER = '>'; |
| 32: | public const OPERATOR_GREATER_EQUAL = '>='; |
| 33: | public const OPERATOR_LESS = '<'; |
| 34: | public const OPERATOR_LESS_EQUAL = '<='; |
| 35: | public const OPERATOR_LIKE = 'LIKE'; |
| 36: | public const OPERATOR_NOT_LIKE = 'NOT LIKE'; |
| 37: | public const OPERATOR_IN = 'IN'; |
| 38: | public const OPERATOR_NOT_IN = 'NOT IN'; |
| 39: | public const OPERATOR_REGEXP = 'REGEXP'; |
| 40: | public const OPERATOR_NOT_REGEXP = 'NOT REGEXP'; |
| 41: | |
| 42: | |
| 43: | protected static $operators = [ |
| 44: | self::OPERATOR_EQUALS => [ |
| 45: | 'negate' => self::OPERATOR_DOESNOT_EQUAL, |
| 46: | 'label' => 'is equal to', |
| 47: | ], |
| 48: | self::OPERATOR_DOESNOT_EQUAL => [ |
| 49: | 'negate' => self::OPERATOR_EQUALS, |
| 50: | 'label' => 'is not equal to', |
| 51: | ], |
| 52: | self::OPERATOR_LESS => [ |
| 53: | 'negate' => self::OPERATOR_GREATER_EQUAL, |
| 54: | 'label' => 'is smaller than', |
| 55: | ], |
| 56: | self::OPERATOR_GREATER => [ |
| 57: | 'negate' => self::OPERATOR_LESS_EQUAL, |
| 58: | 'label' => 'is greater than', |
| 59: | ], |
| 60: | self::OPERATOR_GREATER_EQUAL => [ |
| 61: | 'negate' => self::OPERATOR_LESS, |
| 62: | 'label' => 'is greater or equal to', |
| 63: | ], |
| 64: | self::OPERATOR_LESS_EQUAL => [ |
| 65: | 'negate' => self::OPERATOR_GREATER, |
| 66: | 'label' => 'is smaller or equal to', |
| 67: | ], |
| 68: | self::OPERATOR_LIKE => [ |
| 69: | 'negate' => self::OPERATOR_NOT_LIKE, |
| 70: | 'label' => 'is like', |
| 71: | ], |
| 72: | self::OPERATOR_NOT_LIKE => [ |
| 73: | 'negate' => self::OPERATOR_LIKE, |
| 74: | 'label' => 'is not like', |
| 75: | ], |
| 76: | self::OPERATOR_IN => [ |
| 77: | 'negate' => self::OPERATOR_NOT_IN, |
| 78: | 'label' => 'is one of', |
| 79: | ], |
| 80: | self::OPERATOR_NOT_IN => [ |
| 81: | 'negate' => self::OPERATOR_IN, |
| 82: | 'label' => 'is not one of', |
| 83: | ], |
| 84: | self::OPERATOR_REGEXP => [ |
| 85: | 'negate' => self::OPERATOR_NOT_REGEXP, |
| 86: | 'label' => 'is regular expression', |
| 87: | ], |
| 88: | self::OPERATOR_NOT_REGEXP => [ |
| 89: | 'negate' => self::OPERATOR_REGEXP, |
| 90: | 'label' => 'is not regular expression', |
| 91: | ], |
| 92: | ]; |
| 93: | |
| 94: | |
| 95: | |
| 96: | |
| 97: | |
| 98: | |
| 99: | public function __construct($key, $operator = null, $value = null) |
| 100: | { |
| 101: | if ($key instanceof AbstractScope) { |
| 102: | throw new Exception('Only Scope can contain another conditions'); |
| 103: | } elseif ($key instanceof Field) { |
| 104: | $key = $key->shortName; |
| 105: | } elseif (!is_string($key) && !$key instanceof Expressionable) { |
| 106: | throw new Exception('Field must be a string or an instance of Expressionable'); |
| 107: | } |
| 108: | |
| 109: | if ('func_num_args'() === 2) { |
| 110: | $value = $operator; |
| 111: | $operator = self::OPERATOR_EQUALS; |
| 112: | } |
| 113: | |
| 114: | $this->key = $key; |
| 115: | $this->value = $value; |
| 116: | |
| 117: | if ($operator === null) { |
| 118: | |
| 119: | if (!$key instanceof Expressionable) { |
| 120: | throw new Exception('Operator must be specified'); |
| 121: | } |
| 122: | } else { |
| 123: | $this->operator = strtoupper($operator); |
| 124: | |
| 125: | if (!array_key_exists($this->operator, self::$operators)) { |
| 126: | throw (new Exception('Operator is not supported')) |
| 127: | ->addMoreInfo('operator', $operator); |
| 128: | } |
| 129: | } |
| 130: | |
| 131: | if (is_array($value)) { |
| 132: | foreach ($value as $v) { |
| 133: | if (is_array($v)) { |
| 134: | throw (new Exception('Multi-dimensional array as condition value is not supported')) |
| 135: | ->addMoreInfo('value', $value); |
| 136: | } |
| 137: | } |
| 138: | |
| 139: | if (!in_array($this->operator, [ |
| 140: | self::OPERATOR_EQUALS, |
| 141: | self::OPERATOR_IN, |
| 142: | self::OPERATOR_DOESNOT_EQUAL, |
| 143: | self::OPERATOR_NOT_IN, |
| 144: | ], true)) { |
| 145: | throw (new Exception('Operator is not supported for array condition value')) |
| 146: | ->addMoreInfo('operator', $operator) |
| 147: | ->addMoreInfo('value', $value); |
| 148: | } |
| 149: | } |
| 150: | } |
| 151: | |
| 152: | #[\Override] |
| 153: | protected function onChangeModel(): void |
| 154: | { |
| 155: | $model = $this->getModel(); |
| 156: | if ($model !== null) { |
| 157: | |
| 158: | |
| 159: | |
| 160: | if ($this->operator === self::OPERATOR_EQUALS && !is_array($this->value) |
| 161: | && !$this->value instanceof Expressionable |
| 162: | && !$this->value instanceof Persistence\Array_\Action |
| 163: | ) { |
| 164: | |
| 165: | $field = $this->key; |
| 166: | if (is_string($field) && !str_contains($field, '/')) { |
| 167: | $field = $model->getField($field); |
| 168: | } |
| 169: | |
| 170: | |
| 171: | |
| 172: | |
| 173: | if ($field instanceof Field && $field->shortName !== $field->getOwner()->idField) { |
| 174: | $field->system = true; |
| 175: | $fakePersistence = new Persistence\Array_(); |
| 176: | $valueCloned = $fakePersistence->typecastLoadField($field, $fakePersistence->typecastSaveField($field, $this->value)); |
| 177: | $field->default = $valueCloned; |
| 178: | } |
| 179: | } |
| 180: | } |
| 181: | } |
| 182: | |
| 183: | |
| 184: | |
| 185: | |
| 186: | public function toQueryArguments(): array |
| 187: | { |
| 188: | if ($this->isEmpty()) { |
| 189: | return []; |
| 190: | } |
| 191: | |
| 192: | $field = $this->key; |
| 193: | $operator = $this->operator; |
| 194: | $value = $this->value; |
| 195: | |
| 196: | $model = $this->getModel(); |
| 197: | if ($model !== null) { |
| 198: | if (is_string($field)) { |
| 199: | |
| 200: | |
| 201: | if (str_contains($field, '/')) { |
| 202: | $references = explode('/', $field); |
| 203: | $field = array_pop($references); |
| 204: | |
| 205: | $refModels = []; |
| 206: | $refModel = $model; |
| 207: | foreach ($references as $link) { |
| 208: | $refModel = $refModel->refLink($link); |
| 209: | $refModels[] = $refModel; |
| 210: | } |
| 211: | unset($refModel); |
| 212: | |
| 213: | foreach (array_reverse($refModels) as $refModel) { |
| 214: | if ($field === '#') { |
| 215: | if (is_string($value) && $value === (string) (int) $value) { |
| 216: | $value = (int) $value; |
| 217: | } |
| 218: | |
| 219: | if ($value === 0) { |
| 220: | $field = $refModel->action('exists'); |
| 221: | $value = false; |
| 222: | } elseif ($value === 1 && $operator === self::OPERATOR_GREATER_EQUAL) { |
| 223: | $field = $refModel->action('exists'); |
| 224: | $operator = self::OPERATOR_EQUALS; |
| 225: | $value = true; |
| 226: | } else { |
| 227: | $field = $refModel->action('count'); |
| 228: | } |
| 229: | } else { |
| 230: | $refModel->addCondition($field, $operator, $value); |
| 231: | $field = $refModel->action('exists'); |
| 232: | $operator = self::OPERATOR_EQUALS; |
| 233: | $value = true; |
| 234: | } |
| 235: | } |
| 236: | } else { |
| 237: | $field = $model->getField($field); |
| 238: | } |
| 239: | } |
| 240: | |
| 241: | |
| 242: | if ($field instanceof Field) { |
| 243: | [$field, $operator, $value] = $field->getQueryArguments($operator, $value); |
| 244: | } |
| 245: | |
| 246: | |
| 247: | if (!$operator) { |
| 248: | return [$field]; |
| 249: | } |
| 250: | |
| 251: | |
| 252: | |
| 253: | if ($operator === self::OPERATOR_EQUALS) { |
| 254: | return [$field, $value]; |
| 255: | } |
| 256: | } |
| 257: | |
| 258: | return [$field, $operator, $value]; |
| 259: | } |
| 260: | |
| 261: | #[\Override] |
| 262: | public function isEmpty(): bool |
| 263: | { |
| 264: | return array_filter([$this->key, $this->operator, $this->value]) ? false : true; |
| 265: | } |
| 266: | |
| 267: | #[\Override] |
| 268: | public function clear(): self |
| 269: | { |
| 270: | $this->key = null; |
| 271: | $this->operator = null; |
| 272: | $this->value = null; |
| 273: | |
| 274: | return $this; |
| 275: | } |
| 276: | |
| 277: | #[\Override] |
| 278: | public function negate(): self |
| 279: | { |
| 280: | if (isset(self::$operators[$this->operator]['negate'])) { |
| 281: | $this->operator = self::$operators[$this->operator]['negate']; |
| 282: | } else { |
| 283: | throw (new Exception('Negation of condition is not supported for this operator')) |
| 284: | ->addMoreInfo('operator', $this->operator ?? 'no operator'); |
| 285: | } |
| 286: | |
| 287: | return $this; |
| 288: | } |
| 289: | |
| 290: | #[\Override] |
| 291: | public function toWords(Model $model = null): string |
| 292: | { |
| 293: | if ($model === null) { |
| 294: | $model = $this->getModel(); |
| 295: | } |
| 296: | |
| 297: | if ($model === null) { |
| 298: | throw new Exception('Condition must be associated with Model to convert to words'); |
| 299: | } |
| 300: | |
| 301: | $key = $this->keyToWords($model); |
| 302: | $operator = $this->operatorToWords(); |
| 303: | $value = $this->valueToWords($model, $this->value); |
| 304: | |
| 305: | return trim($key . ' ' . $operator . ' ' . $value); |
| 306: | } |
| 307: | |
| 308: | protected function keyToWords(Model $model): string |
| 309: | { |
| 310: | $words = []; |
| 311: | |
| 312: | $field = $this->key; |
| 313: | if (is_string($field)) { |
| 314: | if (str_contains($field, '/')) { |
| 315: | $references = explode('/', $field); |
| 316: | |
| 317: | $words[] = $model->getModelCaption(); |
| 318: | |
| 319: | $field = array_pop($references); |
| 320: | |
| 321: | foreach ($references as $link) { |
| 322: | $words[] = 'that has reference ' . $this->readableCaption($link); |
| 323: | |
| 324: | $model = $model->refLink($link); |
| 325: | } |
| 326: | |
| 327: | $words[] = 'where'; |
| 328: | |
| 329: | if ($field === '#') { |
| 330: | $words[] = $this->operator ? 'number of records' : 'any referenced record exists'; |
| 331: | } |
| 332: | } |
| 333: | |
| 334: | if ($model->hasField($field)) { |
| 335: | $field = $model->getField($field); |
| 336: | } |
| 337: | } |
| 338: | |
| 339: | if ($field instanceof Field) { |
| 340: | $words[] = $field->getCaption(); |
| 341: | } elseif ($field instanceof Expressionable) { |
| 342: | $words[] = $this->valueToWords($model, $field); |
| 343: | } |
| 344: | |
| 345: | return implode(' ', array_filter($words)); |
| 346: | } |
| 347: | |
| 348: | protected function operatorToWords(): string |
| 349: | { |
| 350: | return $this->operator ? self::$operators[$this->operator]['label'] : ''; |
| 351: | } |
| 352: | |
| 353: | |
| 354: | |
| 355: | |
| 356: | protected function valueToWords(Model $model, $value): string |
| 357: | { |
| 358: | if ($value === null) { |
| 359: | return $this->operator ? 'empty' : ''; |
| 360: | } |
| 361: | |
| 362: | if (is_array($value)) { |
| 363: | $res = []; |
| 364: | foreach ($value as $v) { |
| 365: | $res[] = $this->valueToWords($model, $v); |
| 366: | } |
| 367: | |
| 368: | return implode(' or ', $res); |
| 369: | } |
| 370: | |
| 371: | if (is_object($value)) { |
| 372: | if ($value instanceof Field) { |
| 373: | return $value->getOwner()->getModelCaption() . ' ' . $value->getCaption(); |
| 374: | } |
| 375: | |
| 376: | if ($value instanceof Expressionable) { |
| 377: | return 'expression \'' . $value->getDsqlExpression(new SqliteExpression())->getDebugQuery() . '\''; |
| 378: | } |
| 379: | |
| 380: | return 'object ' . print_r($value, true); |
| 381: | } |
| 382: | |
| 383: | |
| 384: | $field = $this->key; |
| 385: | if (is_string($field)) { |
| 386: | if (str_contains($field, '/')) { |
| 387: | $references = explode('/', $field); |
| 388: | |
| 389: | $field = array_pop($references); |
| 390: | |
| 391: | foreach ($references as $link) { |
| 392: | $model = $model->refLink($link); |
| 393: | } |
| 394: | } |
| 395: | |
| 396: | if ($model->hasField($field)) { |
| 397: | $field = $model->getField($field); |
| 398: | } |
| 399: | } |
| 400: | |
| 401: | |
| 402: | $title = null; |
| 403: | if ($field instanceof Field && $field->hasReference()) { |
| 404: | |
| 405: | $entity = $model->isEntity() ? clone $model : $model->createEntity(); |
| 406: | $entity->set($field->shortName, $value); |
| 407: | |
| 408: | |
| 409: | $title = $entity->ref($field->getReference()->link)->getTitle(); |
| 410: | if ($title === $value) { |
| 411: | $title = null; |
| 412: | } |
| 413: | } |
| 414: | |
| 415: | if (is_bool($value)) { |
| 416: | $valueStr = $value ? 'true' : 'false'; |
| 417: | } elseif (is_int($value)) { |
| 418: | $valueStr = (string) $value; |
| 419: | } elseif (is_float($value)) { |
| 420: | $valueStr = Expression::castFloatToString($value); |
| 421: | } else { |
| 422: | $valueStr = '\'' . (string) $value . '\''; |
| 423: | } |
| 424: | |
| 425: | return $valueStr . ($title !== null ? ' (\'' . $title . '\')' : ''); |
| 426: | } |
| 427: | } |
| 428: | |