1: <?php
2:
3: namespace LaravelUi5\Core\Ui5;
4:
5: use LaravelUi5\Core\Attributes\Parameter;
6: use LaravelUi5\Core\Enums\ParameterSource;
7: use LaravelUi5\Core\Exceptions\InvalidHttpMethodActionException;
8: use LaravelUi5\Core\Exceptions\InvalidModuleException;
9: use LaravelUi5\Core\Exceptions\InvalidParameterSourceException;
10: use LaravelUi5\Core\Ui5\Contracts\LaravelUi5ManifestInterface;
11: use LaravelUi5\Core\Ui5\Contracts\LaravelUi5ManifestKeys;
12: use LaravelUi5\Core\Ui5\Contracts\ParameterizableInterface;
13: use LaravelUi5\Core\Ui5\Contracts\Ui5ModuleInterface;
14: use LaravelUi5\Core\Ui5\Contracts\Ui5RuntimeInterface;
15: use ReflectionClass;
16: use RuntimeException;
17:
18: /**
19: * Base class for building a `laravel.ui5` manifest fragment.
20: *
21: * Automatically provides core sections (actions, reports, routes, meta)
22: * and allows apps to augment the manifest via `augmentFragment()`.
23: *
24: * All keys must be defined in LaravelUi5ManifestKeys. Unknown keys will throw.
25: */
26: abstract class AbstractManifest implements LaravelUi5ManifestInterface
27: {
28:
29: public function __construct(protected Ui5RuntimeInterface $registry)
30: {
31: }
32:
33: /**
34: * Returns the complete, validated `laravel.ui5` manifest fragment.
35: *
36: * This includes core sections (actions, reports, routes, meta) and
37: * any application-specific extensions provided by `augmentFragment()`.
38: *
39: * @param string $module the module slug
40: *
41: * @return array<string, mixed>
42: */
43: public function getFragment(string $module): array
44: {
45: if (!$this->registry->hasModule($module)) {
46: throw new InvalidModuleException($module);
47: }
48:
49: $resolved = $this->registry->getModule($module);
50: $namespace = $resolved->getArtifactRoot()->getNamespace();
51:
52: $core = [
53: LaravelUi5ManifestKeys::META => $this->buildMeta(),
54: LaravelUi5ManifestKeys::ROUTES => $this->buildRoutes(),
55: LaravelUi5ManifestKeys::ACTIONS => $this->buildActions($resolved),
56: LaravelUi5ManifestKeys::RESOURCES => $this->buildResources($resolved),
57: LaravelUi5ManifestKeys::INTENTS => $this->buildIntents($module),
58: ];
59:
60: $fragment = array_merge($core, $this->enhanceFragment($module));
61:
62: $unknownKeys = array_diff(array_keys($fragment), LaravelUi5ManifestKeys::all());
63: if (!empty($unknownKeys)) {
64: throw new RuntimeException(
65: 'Unknown manifest key(s) in laravel.ui5 fragment: ' . implode(', ', $unknownKeys)
66: );
67: }
68:
69: return array_filter($fragment, fn($value) => !empty($value));
70: }
71:
72: /**
73: * Optional hook to extend the manifest with abilities, roles, settings, etc.
74: *
75: * Override this in your subclass to provide domain-specific config.
76: *
77: * @return array<string, mixed>
78: */
79: abstract protected function enhanceFragment(string $module): array;
80:
81: /**
82: * Returns static metadata like version, client, branding flags etc.
83: *
84: * @return array<string, mixed>
85: */
86: private function buildMeta(): array
87: {
88: return ['generator' => 'LaravelUi5 Core'] + config('ui5.meta', []);
89: }
90:
91: /**
92: * Returns commonly used routes like privacy, terms, login, logout.
93: *
94: * @return array<string, string>
95: */
96: private function buildRoutes(): array
97: {
98: return array_map(fn($name) => route($name), config('ui5.routes', []));
99: }
100:
101: /**
102: * Returns the list of backend actions provided by this app.
103: *
104: * Override if needed.
105: *
106: * @return array<string, array{method: string, url: string}>
107: */
108: private function buildActions(Ui5ModuleInterface $module): array
109: {
110: $actions = [];
111: foreach ($module->getActions() as $action) {
112: if (!$action->getMethod()->isValidUi5ActionMethod()) {
113: throw new InvalidHttpMethodActionException($action->getNamespace(), $action->getMethod()->label());
114: }
115:
116: $uri = collect($this->getPathParameters($action->getHandler()))
117: ->map(fn(string $parameter) => "/{{$parameter}}")
118: ->implode('');
119:
120: $actions[$action->getSlug()] = [
121: 'method' => $action->getMethod()->label(),
122: 'url' => "/ui5/{$this->registry->slugFor($action)}{$uri}"
123: ];
124: };
125:
126: return $actions;
127: }
128:
129: private function buildResources(Ui5ModuleInterface $module): array
130: {
131: $resources = [];
132: foreach ($module->getResources() as $resource) {
133: $provider = $resource->getProvider();
134:
135: if ($provider instanceof ParameterizableInterface) {
136: $uri = collect($this->getPathParameters($provider))
137: ->map(fn(string $parameter) => "/{{$parameter}}")
138: ->implode('');
139:
140: $resources[$resource->getSlug()] = [
141: 'method' => 'GET',
142: 'url' => "/ui5/{$this->registry->slugFor($resource)}{$uri}"
143: ];
144: }
145: }
146:
147: return $resources;
148: }
149:
150: private function buildIntents(string $module): array
151: {
152: return $this->registry->resolveIntents($module);
153: }
154:
155: /** -- Helper ---------------------------------------------------------- */
156:
157: private function getPathParameters(ParameterizableInterface $target): array
158: {
159: $reflection = new ReflectionClass($target);
160: $attributes = $reflection->getAttributes(Parameter::class);
161: $parameters = [];
162: foreach ($attributes as $attr) {
163: /** @var Parameter $attribute */
164: $attribute = $attr->newInstance();
165: if (ParameterSource::Path === $attribute->source) {
166: $parameters[] = $attribute->uriKey;
167: } else {
168: throw new InvalidParameterSourceException($attribute->name, $attribute->source->label());
169: }
170: }
171: return $parameters;
172: }
173: }
174: