1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Protocol\Execution;
6:
7: use LaravelUi5\OData\Http\ODataResponse;
8: use LaravelUi5\OData\Protocol\Planning\EntitySetQueryPlan;
9: use LaravelUi5\OData\Protocol\Planning\SelectList;
10: use LaravelUi5\OData\Service\Contracts\RuntimeSchemaInterface;
11:
12: /**
13: * Handles EntitySetQueryPlan — produces a streamed OData JSON collection.
14: *
15: * Emits:
16: * {"@odata.context":"<serviceRoot>$metadata#<SetName>","value":[
17: * {"id":1,"name":"..."},
18: * ...
19: * ]}
20: *
21: * Rows are streamed directly from the resolver generator — no buffering.
22: */
23: final readonly class EntitySetHandler
24: {
25: public function __construct(
26: private RuntimeSchemaInterface $schema,
27: private string $serviceRoot,
28: ) {}
29:
30: public function handle(EntitySetQueryPlan $plan): ODataResponse
31: {
32: $context = $this->serviceRoot . '$metadata#' . $plan->target->getName()
33: . SelectHelper::contextFragment($plan->select);
34: $resolver = $this->schema->getResolver($plan->target);
35: $selectKeys = SelectHelper::allowedKeys($plan->select, $plan->expand, $plan->compute);
36:
37: // Server-driven paging: maxPageSize applies when no explicit $top.
38: $pageSize = ($plan->maxPageSize !== null && $plan->top === null)
39: ? $plan->maxPageSize
40: : null;
41:
42: $headers = [
43: 'Content-Type' => 'application/json;odata.metadata=minimal;charset=utf-8',
44: 'OData-Version' => '4.0',
45: ];
46:
47: if ($plan->maxPageSize !== null) {
48: $headers['Preference-Applied'] = 'odata.maxpagesize=' . $plan->maxPageSize;
49: }
50:
51: $response = new ODataResponse(null, 200, $headers);
52:
53: $count = $plan->count ? $resolver->count($plan) : null;
54: $serviceRoot = $this->serviceRoot;
55: $setName = $plan->target->getName();
56: $coercion = new RowCoercion($plan->target->getEntityType());
57:
58: $response->setCallback(static function () use ($context, $resolver, $plan, $selectKeys, $count, $pageSize, $serviceRoot, $setName, $coercion): void {
59: $generator = $resolver->resolve($plan);
60:
61: echo '{"@odata.context":' . json_encode($context);
62:
63: if ($count !== null) {
64: echo ',"@odata.count":' . $count;
65: }
66:
67: echo ',"value":[';
68:
69: $first = true;
70: $emitted = 0;
71: $hasMore = false;
72:
73: foreach ($generator as $row) {
74: if ($pageSize !== null && $emitted >= $pageSize) {
75: $hasMore = true;
76: break;
77: }
78:
79: if (!$first) {
80: echo ',';
81: }
82: $row = $selectKeys !== null ? array_intersect_key($row, $selectKeys) : $row;
83: $row = $coercion->apply($row);
84: echo json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
85: $first = false;
86: $emitted++;
87: }
88:
89: echo ']';
90:
91: if ($hasMore) {
92: $skip = ($plan->skip ?? 0) + $emitted;
93: $nextLink = $serviceRoot . $setName . '?$skip=' . $skip;
94: echo ',"@odata.nextLink":' . json_encode($nextLink);
95: }
96:
97: echo '}';
98: });
99:
100: return $response;
101: }
102: }
103: