1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace LaravelUi5\OData\Service;
6:
7: use Illuminate\Database\Query\Builder;
8: use Illuminate\Support\Str;
9: use LaravelUi5\OData\Driver\Sql\SqlEntitySetResolver;
10: use LaravelUi5\OData\Edm\Container\EnumType;
11: use LaravelUi5\OData\Edm\EdmPrimitiveType;
12: use LaravelUi5\OData\Edm\Contracts\Type\EntityTypeInterface;
13: use LaravelUi5\OData\Edm\Property\Property;
14: use LaravelUi5\OData\Edm\Type\EntityType;
15: use LaravelUi5\OData\Edm\Type\PrimitiveType;
16: use LaravelUi5\OData\Service\Contracts\CustomEntitySetInterface;
17: use LaravelUi5\OData\Service\Contracts\SqlQueryInterface;
18:
19: /**
20: * Declarative base class for SQL-backed custom entity sets.
21: *
22: * Subclasses declare their schema via {@see columns()} and {@see key()},
23: * and provide the SQL source via {@see query()}. The entity type is
24: * assembled automatically — no manual EDM construction needed.
25: *
26: * Example:
27: *
28: * final readonly class BillableProjects extends AbstractEntitySet
29: * {
30: * public function entitySetName(): string { return 'BillableProjects'; }
31: *
32: * public function key(): array { return ['project_id']; }
33: *
34: * public function columns(): array
35: * {
36: * return [
37: * 'project_id' => EdmPrimitiveType::Int64,
38: * 'customer' => EdmPrimitiveType::String,
39: * 'tier' => LicenseTier::class, // int-backed PHP enum
40: * 'hours_posted' => EdmPrimitiveType::Double,
41: * ];
42: * }
43: *
44: * public function query(): Builder
45: * {
46: * return DB::query()->fromSub($sql, 't');
47: * }
48: * }
49: *
50: * The entity type name is derived by singularizing {@see entitySetName()}
51: * (e.g. BillableProjects → BillableProject). Override {@see entityType()}
52: * for full control over entity type construction (custom names, navigation
53: * properties, annotations, non-standard key configurations).
54: */
55: abstract readonly class AbstractEntitySet extends SqlEntitySetResolver implements CustomEntitySetInterface, SqlQueryInterface
56: {
57: public function __construct()
58: {
59: parent::__construct($this);
60: }
61:
62: /**
63: * Flat column definitions.
64: *
65: * Each value is either an {@see EdmPrimitiveType} case or the class-string
66: * of an int-backed PHP enum (auto-projected to an `Edm.EnumType`).
67: *
68: * @return array<string, EdmPrimitiveType|class-string<\BackedEnum>>
69: */
70: abstract public function columns(): array;
71:
72: /**
73: * Primary key column name(s). Defaults to the first column in columns().
74: *
75: * Override for composite keys: ['tenant_id', 'project_id'].
76: *
77: * @return list<string>
78: */
79: public function key(): array
80: {
81: return [array_key_first($this->columns())];
82: }
83:
84: /**
85: * Builds the entity type from columns() + key() + entitySetName().
86: *
87: * The entity type name is the singular form of entitySetName().
88: * Override this method for full control (nav props, annotations, etc.).
89: */
90: public function entityType(string $namespace): EntityTypeInterface
91: {
92: $properties = [];
93:
94: foreach ($this->columns() as $name => $type) {
95: $resolved = $type instanceof EdmPrimitiveType
96: ? new PrimitiveType($type)
97: : EnumType::fromBackedEnum($namespace, $type);
98: $properties[$name] = new Property($name, $resolved);
99: }
100:
101: $keyProps = array_values(
102: array_intersect_key($properties, array_flip($this->key())),
103: );
104:
105: return new EntityType(
106: namespace: $namespace,
107: name: Str::singular($this->entitySetName()),
108: key: $keyProps,
109: declaredProperties: array_values($properties),
110: );
111: }
112: }
113: