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