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