1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Protocol\Parser;
6:
7: use LaravelUi5\OData\Edm\Contracts\Property\PropertyInterface;
8: use LaravelUi5\OData\Edm\Contracts\Type\EntityTypeInterface;
9: use LaravelUi5\OData\Exception\BadRequestException;
10: use LaravelUi5\OData\Protocol\Planning\Expression\BinaryExpression;
11: use LaravelUi5\OData\Protocol\Planning\Expression\BinaryOperator;
12: use LaravelUi5\OData\Protocol\Planning\Expression\FilterExpression;
13: use LaravelUi5\OData\Protocol\Planning\Expression\FunctionCallExpression;
14: use LaravelUi5\OData\Protocol\Planning\Expression\LambdaExpression;
15: use LaravelUi5\OData\Protocol\Planning\Expression\LiteralExpression;
16: use LaravelUi5\OData\Protocol\Planning\Expression\NullLiteralExpression;
17: use LaravelUi5\OData\Protocol\Planning\Expression\PropertyPathExpression;
18: use LaravelUi5\OData\Protocol\Planning\Expression\UnaryExpression;
19:
20: /**
21: * Resolves unresolved string property names in a FilterExpression tree
22: * against an EntityTypeInterface, and applies type hints from properties
23: * to adjacent literals.
24: */
25: final readonly class PropertyResolver
26: {
27: /**
28: * Resolve all property names and apply type hints in the given expression.
29: */
30: public function resolve(FilterExpression $expr, EntityTypeInterface $entityType): FilterExpression
31: {
32: return $this->walk($expr, $entityType, null);
33: }
34:
35: private function walk(FilterExpression $expr, EntityTypeInterface $entityType, ?string $hintEdmType): FilterExpression
36: {
37: if ($expr instanceof PropertyPathExpression) {
38: return $this->resolveProperty($expr, $entityType);
39: }
40:
41: if ($expr instanceof LiteralExpression && $hintEdmType !== null) {
42: return new LiteralExpression($expr->value, $hintEdmType);
43: }
44:
45: if ($expr instanceof NullLiteralExpression) {
46: return $expr;
47: }
48:
49: if ($expr instanceof LiteralExpression) {
50: return $expr;
51: }
52:
53: if ($expr instanceof BinaryExpression) {
54: return $this->resolveBinary($expr, $entityType);
55: }
56:
57: if ($expr instanceof UnaryExpression) {
58: return new UnaryExpression(
59: $expr->operator,
60: $this->walk($expr->operand, $entityType, null),
61: );
62: }
63:
64: if ($expr instanceof FunctionCallExpression) {
65: $resolvedArgs = array_map(
66: fn(FilterExpression $arg) => $this->walk($arg, $entityType, null),
67: $expr->arguments,
68: );
69: return new FunctionCallExpression($expr->name, $resolvedArgs);
70: }
71:
72: if ($expr instanceof LambdaExpression) {
73: return $this->resolveLambda($expr, $entityType);
74: }
75:
76: return $expr;
77: }
78:
79: private function resolveBinary(BinaryExpression $expr, EntityTypeInterface $entityType): BinaryExpression
80: {
81: $left = $this->walk($expr->left, $entityType, null);
82:
83: // Infer type hint from left-side property for comparison operators
84: $hintType = null;
85: if ($this->isComparisonOperator($expr->operator) && $left instanceof PropertyPathExpression) {
86: $last = $left->segments[count($left->segments) - 1] ?? null;
87: if ($last instanceof PropertyInterface) {
88: $hintType = $last->getType()->getQualifiedName();
89: }
90: }
91:
92: $right = $this->walk($expr->right, $entityType, $hintType);
93:
94: return new BinaryExpression($left, $expr->operator, $right);
95: }
96:
97: private function isComparisonOperator(BinaryOperator $op): bool
98: {
99: return match ($op) {
100: BinaryOperator::Eq, BinaryOperator::Ne,
101: BinaryOperator::Gt, BinaryOperator::Ge,
102: BinaryOperator::Lt, BinaryOperator::Le,
103: BinaryOperator::Has, BinaryOperator::In => true,
104: default => false,
105: };
106: }
107:
108: private function resolveLambda(LambdaExpression $expr, EntityTypeInterface $entityType): LambdaExpression
109: {
110: // Resolve the navigation property on the collection path
111: $navPropName = $expr->collection->segments[0] ?? null;
112: if (is_string($navPropName)) {
113: $navProperty = $entityType->getNavigationProperty($navPropName);
114: if ($navProperty === null) {
115: throw new BadRequestException(
116: 'unknown_navigation_property',
117: "Unknown navigation property in lambda: {$navPropName}"
118: );
119: }
120:
121: $collection = new PropertyPathExpression([$navProperty]);
122: $predicate = $this->walk($expr->predicate, $navProperty->getTargetType(), null);
123:
124: return new LambdaExpression($collection, $expr->variable, $predicate, $expr->operator);
125: }
126:
127: return $expr;
128: }
129:
130: private function resolveProperty(PropertyPathExpression $expr, EntityTypeInterface $entityType): PropertyPathExpression
131: {
132: $segments = [];
133:
134: foreach ($expr->segments as $segment) {
135: if (is_string($segment)) {
136: $property = $entityType->getProperty($segment);
137: if ($property === null) {
138: throw new BadRequestException(
139: 'unknown_property',
140: "Unknown property in \$filter: {$segment}"
141: );
142: }
143: $segments[] = $property;
144: } else {
145: $segments[] = $segment;
146: }
147: }
148:
149: return new PropertyPathExpression($segments);
150: }
151: }
152: