diff --git a/README.md b/README.md index 6e2605a..6376d26 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ return [ 'route' => [ 'panel_prefix' => true, 'use_resource_middlewares' => false, + 'default_transformer_name' => 'default', + 'api_version_method' => 'headers', // options: ['path', 'query', 'headers'] + 'api_version_parameter_name' => env('API_VERSION_PARAMETER_NAME', 'version'), + 'api_transformer_header' => env('API_TRANSFORMER_HEADER', 'X-API-TRANSFORMER'), ], 'tenancy' => [ 'enabled' => false, @@ -98,7 +102,6 @@ Token Resource is protected by TokenPolicy. You can disable it by publishing the ### Filtering & Allowed Field We used `"spatie/laravel-query-builder": "^5.3"` to handle query selecting, sorting and filtering. Check out [the spatie/laravel-query-builder documentation](https://spatie.be/docs/laravel-query-builder/v5/introduction) for more information. - In order to allow modifying the query for your model you can implement the `HasAllowedFields`, `HasAllowedSorts` and `HasAllowedFilters` Contracts in your model. ```php @@ -178,8 +181,10 @@ next step you need to edit & add it to your Resource class BlogResource extends Resource { - ... - public static function getApiTransformer() + /** + * @return string|null + */ + public static function getApiTransformer(): ?string { return BlogTransformer::class; } @@ -187,6 +192,95 @@ next step you need to edit & add it to your Resource } ``` +### Multiple Transformers via HTTP Header + +Sometimes you need a different response structure of your API resource. Then you can create multiple Transformers as described above. In your API request you need to set an extra http header with the value of your Transformer class name. +You can specify the name/key of the HTTP Header in the config `route.api_transformer_header`. + +By default the HTTP Header is called `X-API-TRANSFORMER`. You could also override it in your .env config file with the parameter: `API_TRANSFORMER_HEADER`. After you set and know the http header you need to create some extra Transformers where the structure is different as needed. You have to register all the extra transformers in your Resource like so: + +```php +use App\Filament\Resources\BlogResource\Api\Transformers\BlogTransformer; +use App\Filament\Resources\BlogResource\Api\Transformers\ModifiedBlogTransformer; +use App\Filament\Resources\BlogResource\Api\Transformers\ExtraBlogColumnsTransformer; + +class BlogResource extends Resource + { + /** + * @return array + */ + public static function apiTransformers(): array + { + return + [ + 'blog' => BlogTransformer::class, + 'mod-blog' => ModifiedBlogTransformer::class, + 'extra-blog-column' => ExtraBlogColumnsTransformer::class, + ... etc. + ] + } + ... + } + +``` + +Now you can use the extra HTTP HEADER `X-API-TRANSFORMER` in your request with the name of the transformer class. + +Here an example in guzzle: + +```php +$client->request('GET', '/api/blogs', [ + 'headers' => [ + 'X-API-TRANSFORMER' => 'ModifiedBlogTransformer' + ] +]); + +or + +$client->request('GET', '/api/blogs', [ + 'headers' => [ + 'X-API-TRANSFORMER' => 'ExtraBlogColumnsTransformer' + ] +]); +``` + +This way the correct transformer will be used to give you the correct response json. + +### Multiple Transformers via URL Path or URL Query + +Alternative methods could also be used, like using as a prefix in the URL path or in the URL Query. + +You can set the method in the config `route.api_version_method` to 'path' or 'query'. You can also set the default Transformername via `route.default_transformer_name` (defaults to 'default'). + +When you set `route.api_version_method` to 'path' then you can use the name of the transformer in the first segment of the API URL. the name of that fist segment is the key which you have defined in the `apiTransformers()` function. + +So for example if you want to use the `'mod-blog'` transformer in your api response. the url might look like this: +'' + +It will always be in front of all other options like `tenant` or `panel` names in the url. + +With this method you could use API versioning like `/api/v1` where `v1` is an item in the `apiTransformers()` function. + +like so: + +```php + +public static function apiTransformers(): array + { + return + [ + 'v1' => VersionOneTransformer::class, + ... etc. + ] + } +``` + +Another method is using `route.api_version_method` to 'query'. This way you can add an extra parameter in the URL with the name which you defined in your config under `route.api_version_parameter_name` +by default this parameter is `version`. With this config the URL would look like this: +'' + +IMPORTANT: You can only use one method for this package for API versioning. + ### Group Name & Prefix You can edit prefix & group route name as you want, default this plugin use model singular label; @@ -270,6 +364,154 @@ class PaginationHandler extends Handlers { } ``` +## Swagger Api Docs Generation + +It is possible to generate Swagger API docs with this package. You have to make sure you have the following dependencies: + +```bash +composer require darkaonline/l5-swagger +``` + +And make sure you install it correctly according to their [installation manual](https://github.com/DarkaOnLine/L5-Swagger/wiki/Installation-&-Configuration#installation). +In development we recommend setting the config in `l5-swagger.php` `defaults.generate_always` to `true`. + +When generating Api Swagger Docs for an Filament Resource it is required to define a Transformer. Otherwise the generator does not know how your resource entity types are being handled. What the response format and types look like. + +Therefor you should always create a Transformer, which is explained above at the section [Transform API Response](#transform-api-response). + +Then you can use the following command to generate API docs for your resources: + +```bash +php artisan make:filament-api-docs {resource?} {namespace?} +``` + +so for example: + +```bash +php artisan make:filament-api-docs BlogResource +``` + +The CLI command accepts two optional parameters: `{resource?}` and `{namespace?}`. +By default the Swagger API Docs will be placed in `app/Virtual/Filament/Resources` folder under their own resource name. + +For example the BlogResource Api Docs wil be in the following folder `app/Virtual/Filament/Resource/BlogResource`. + +First it will check if you have an existing the Swagger Docs Server config. This is a file `ApiDocsController.php` and resides in `app/Virtual/Filament/Resources`. +It holds some general information about your swagger API Docs server. All generated files can be manual edited afterwards. +Regenerating an API Docs Serverinfo or Resource will always ask you if you want to override the existing file. + +When done, you can go to the defined swagger documentation URL as defined in `l5-swagger.php` config as `documentations.routes.api`. + +If you want to generate the Api Docs manually because in your `l5-swagger.php` config you have set `defatuls.generate_always` to `false` you can do so by invoking: + +```bash +php artisan l5-swagger:generate +``` + +After you have generated the Swagger API Docs, you can add your required Transformer properties as needed. + +by default as an example it will generate this when you use a BlogTransformer: + +```php +class BlogTransformer { + + #[OAT\Property( + property: "data", + type: "array", + items: new OAT\Items( + properties: [ + new OAT\Property(property: "id", type: "integer", title: "ID", description: "id of the blog", example: ""), + + // Add your own properties corresponding to the BlogTransformer + ] + ), + )] + public $data; + + ... +} + +``` + +You can find more about all possible properties at + +### Laravel-Data package integration (optional) + +It is possible to use Spatie/Laravel-data package to autogenerate the correct data model fields for a transformer. But first you need to setup your laravel-data package see [more info package spatie/laravel-data](https://spatie.be/docs/laravel-data) for installation instructions. + +For the Generator to work your DTO needs some attributes where i can derives the properties from. +Basic properties like the property name and type will be fetched using Reflection class methods. +But some extra optional properties like: `description`, `example` are not available in the model or DTO. +By default the following attributes can be added: `title`, `description` and `example`. all other fields you want to include have to be used as an array in the `extraProperties` parameter. See the last item in the example. +So this is how you can implement those: + +```php + +namespace App\Data; + +use Rupadana\ApiService\Attributes\ApiPropertyInfo; // <-- add this Attribute to you DTO +use Spatie\LaravelData\Data; + +class BlogData extends Data +{ + public function __construct( + #[ApiPropertyInfo(description: 'ID of the Blog DTO', example: '')] + public ?int $id, + #[ApiPropertyInfo(description: 'Name of the Blog', example: '')] + public string $name, + #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', ref: "ImageBlogSchema", oneOf: '[new OAT\Schema(type:"string"), new OAT\Schema(type:"integer")]'] + )] + public string $image, + ) { + } +} + +``` + +As you can see you can add attributes above each property. this way when generating the transformer Api Docs it will add these information. + +The result of the Api Docs generation of the Transformer(s) will look like this: + +```php +namespace App\Virtual\Filament\Resources\BlogResource\Transformers; + + +use OpenApi\Attributes as OAT; + +#[OAT\Schema( + schema: "BlogTransformer", + title: "BlogTransformer", + description: "Brands API Transformer", + xml: new OAT\Xml(name: "BlogTransformer"), +)] + +class BlogTransformer { + + #[OAT\Property( + property: "data", + type: "array", + items: new OAT\Items( + properties: [ + new OAT\Property(property: 'id', type: 'int', title: 'id', description: 'ID of the Blog DTO', example: ''), + new OAT\Property(property: 'name', type: 'string', title: 'name', description: 'Name of the Blog', example: ''), + new OAT\Property( + property: 'image', + type: 'string', + title: 'image', + description: 'Image Url of the Blog', + example: '', + ref: "MyBlogSchema", + oneOf: [ + new OAT\Schema(type="string"), + new OAT\Schema(type="integer") + ]), + ] + ), + )], + public $data; + ... +``` + ## License The MIT License (MIT). diff --git a/config/api-service.php b/config/api-service.php index e999886..34c96a4 100644 --- a/config/api-service.php +++ b/config/api-service.php @@ -1,5 +1,7 @@ [ 'token' => [ @@ -17,9 +19,13 @@ 'route' => [ 'panel_prefix' => true, 'use_resource_middlewares' => false, + 'default_transformer_name' => 'default', + 'api_version_method' => 'headers', // options: ['path', 'query', 'headers'] + 'api_version_parameter_name' => env('API_VERSION_PARAMETER_NAME', 'version'), + 'api_transformer_header' => env('API_TRANSFORMER_HEADER', 'X-API-TRANSFORMER'), ], 'tenancy' => [ 'enabled' => false, 'awareness' => false, - ], + ] ]; diff --git a/routes/api.php b/routes/api.php index 7488aa4..c9fb57e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,7 +22,7 @@ $tenantSlugAttribute = $panel->getTenantSlugAttribute(); $apiServicePlugin = $panel->getPlugin('api-service'); $middlewares = $apiServicePlugin->getMiddlewares(); - $panelRoutePrefix = ApiService::isRoutePrefixedByPanel() ? '{panel}' : ''; + $panelRoutePrefix = ApiService::isRoutePrefixedByPanel() ? '{panel}/' : ''; $panelNamePrefix = $panelRoutePrefix ? $panel->getId() . '.' : ''; if ( @@ -30,21 +30,21 @@ ApiService::isTenancyEnabled() && ApiService::tenancyAwareness() ) { - Route::prefix($panelRoutePrefix . '/' . (($tenantRoutePrefix) ? "{$tenantRoutePrefix}/" : '') . '{tenant' . (($tenantSlugAttribute) ? ":{$tenantSlugAttribute}" : '') . '}') - ->name($panelNamePrefix) + $routePrefix = $panelRoutePrefix . (($tenantRoutePrefix) ? "{$tenantRoutePrefix}/" : '') . '{tenant' . (($tenantSlugAttribute) ? ":{$tenantSlugAttribute}" : '') . '}/'; + Route::name($panelNamePrefix) ->middleware($middlewares) - ->group(function () use ($panel, $apiServicePlugin) { - $apiServicePlugin->route($panel); + ->group(function () use ($panel, $routePrefix, $apiServicePlugin) { + $apiServicePlugin->route($panel, $routePrefix); }); } if (! ApiService::tenancyAwareness()) { - Route::prefix($panelRoutePrefix) - ->name($panelNamePrefix) + $routePrefix = $panelRoutePrefix; + Route::name($panelNamePrefix) ->middleware($middlewares) - ->group(function () use ($panel) { + ->group(function () use ($panel, $routePrefix, $apiServicePlugin) { $apiServicePlugin = $panel->getPlugin('api-service'); - $apiServicePlugin->route($panel); + $apiServicePlugin->route($panel, $routePrefix); }); } } catch (Exception $e) { diff --git a/src/ApiService.php b/src/ApiService.php index 0e65028..93f310d 100644 --- a/src/ApiService.php +++ b/src/ApiService.php @@ -17,6 +17,8 @@ class ApiService protected static ?string $resource = null; protected static ?string $groupRouteName = null; + protected static ?string $versionPrefix = ''; + /** * Key Name for query Get * This is used in conditions using slugs in the search @@ -33,20 +35,55 @@ public static function getResource() return static::$resource; } - public static function registerRoutes(Panel $panel) + public static function registerRoutes(Panel $panel, string $baseRoutePrefix) { + $versionPrefix = $baseRoutePrefix; + $slug = static::getResource()::getSlug(); - $name = (string) str(static::$groupRouteName ?? $slug) - ->replace('/', '.') - ->append('.'); + if (static::getApiVersionMethod() === 'path') { + + $transformers = static::getResource()::apiTransformers(); + + foreach ($transformers as $transKey => $transformer) { + + $versionPrefix = '{' . self::getApiVersionParameterName() . '}/'; + $versionPrefix .= $baseRoutePrefix; + + $namePrefix = str($transKey)->kebab() . '/'; + $name = (string) str($namePrefix . (static::$groupRouteName ?? $slug)) + ->replace('/', '.') + ->append('.'); + + static::generateResourceRoutes($panel, $name, $versionPrefix); + } + + } else { + + $name = (string) str(static::$groupRouteName ?? $slug) + ->replace('/', '.') + ->append('.'); + + static::generateResourceRoutes($panel, $name, $versionPrefix); + } + } + + public static function handlers(): array + { + return []; + } + + public static function generateResourceRoutes(Panel $panel, string $name, ?string $versionPrefix) + { + + $slug = static::getResource()::getSlug(); $resourceRouteMiddlewares = static::useResourceMiddlewares() ? static::getResource()::getRouteMiddleware($panel) : []; Route::name($name) ->middleware($resourceRouteMiddlewares) - ->prefix(static::$groupRouteName ?? $slug) + ->prefix($versionPrefix . (static::$groupRouteName ?? $slug)) ->group(function (Router $route) { foreach (static::handlers() as $key => $handler) { app($handler)->route($route); @@ -54,11 +91,6 @@ public static function registerRoutes(Panel $panel) }); } - public static function handlers(): array - { - return []; - } - public static function isRoutePrefixedByPanel(): bool { return config('api-service.route.panel_prefix', true); @@ -68,4 +100,19 @@ public static function useResourceMiddlewares(): bool { return config('api-service.route.use_resource_middlewares', false); } + + public static function getDefaultTransformerName(): string + { + return config('api-service.route.default_transformer_name', 'default'); + } + + public static function getApiVersionMethod(): string + { + return config('api-service.route.api_version_method', 'headers'); + } + + public static function getApiVersionParameterName(): string + { + return config('api-service.route.api_version_parameter_name', 'version'); + } } diff --git a/src/ApiServicePlugin.php b/src/ApiServicePlugin.php index b168998..0600b15 100644 --- a/src/ApiServicePlugin.php +++ b/src/ApiServicePlugin.php @@ -60,7 +60,7 @@ public static function getAbilities(Panel $panel): array return $abilities; } - public function route(Panel $panel): void + public function route(Panel $panel, string $baseRoutePrefix): void { $resources = $panel->getResources(); @@ -70,7 +70,7 @@ public function route(Panel $panel): void $apiServiceClass = $resource . '\\Api\\' . $resourceName . 'ApiService'; - app($apiServiceClass)->registerRoutes($panel); + app($apiServiceClass)->registerRoutes($panel, $baseRoutePrefix); } catch (Exception $e) { } } diff --git a/src/ApiServiceServiceProvider.php b/src/ApiServiceServiceProvider.php index 1383b41..17df07e 100644 --- a/src/ApiServiceServiceProvider.php +++ b/src/ApiServiceServiceProvider.php @@ -7,6 +7,7 @@ use Filament\Support\Facades\FilamentIcon; use Laravel\Sanctum\Http\Middleware\CheckAbilities; use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; +use Rupadana\ApiService\Commands\MakeApiDocsCommand; use Rupadana\ApiService\Commands\MakeApiHandlerCommand; use Rupadana\ApiService\Commands\MakeApiServiceCommand; use Rupadana\ApiService\Commands\MakeApiTransformerCommand; @@ -101,6 +102,7 @@ protected function getCommands(): array MakeApiHandlerCommand::class, MakeApiServiceCommand::class, MakeApiTransformerCommand::class, + MakeApiDocsCommand::class, ]; } diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php new file mode 100644 index 0000000..8f2c056 --- /dev/null +++ b/src/Attributes/ApiPropertyInfo.php @@ -0,0 +1,26 @@ +checkForCollision([$baseServerPath . '/' . $serverFile]); + + // ApiDocsController exists? + if (!$isNotInstalled) { + + $this->components->info("Please provide basic API Docs information."); + $this->components->info("All API Docs Resources will be placed in the app Virtual folder."); + + $serverTitle = text( + label: 'Give the API Docs Server a name...', + placeholder: 'API Documentation', + default: 'API Documentation', + required: true, + ); + + $serverVersion = text( + label: 'Starting version of API Docs...', + placeholder: '0.1', + default: '0.1', + required: true, + ); + + $serverContactName = text( + label: 'API Contact Name', + placeholder: 'your name', + default: '', + required: false, + ); + + $serverContactEmail = text( + label: 'API Contact E-mail', + placeholder: 'your@email.com', + default: '', + required: false, + ); + + $serverTerms = text( + label: 'API Terms of Service url', + placeholder: config('app.url') . '/terms-of-service', + default: '', + ); + + $this->createDirectory('Virtual/Filament/Resources'); + + $this->copyStubToApp('Api/ApiDocsController', $baseServerPath . '/' . $serverFile, [ + 'namespace' => $serverNameSpace, + 'title' => $serverTitle, + 'version' => $serverVersion, + 'contactName' => $serverContactName, + 'contactEmail' => $serverContactEmail, + 'terms' => $serverTerms, + 'licenseName' => 'MIT License', + 'licenseUrl' => 'https://opensource.org/license/mit', + ]); + + // Generate DefaultTransformer + $transformerClass = 'DefaultTransformer'; + + $stubVarsDefaultTransformer = [ + 'namespace' => $serverNameSpace, + 'modelClass' => 'Default', + 'resourceClass' => null, + 'transformerName' => $transformerClass, + ]; + + if (!$this->checkForCollision(["{$baseServerPath}/{$transformerClass}.php"])) { + $this->copyStubToApp("Api/Transformer", $baseServerPath . '/' . $transformerClass . '.php', $stubVarsDefaultTransformer); + } + } + + $model = (string) str($this->argument('resource') ?? text( + label: 'What is the Resource name?', + placeholder: 'Blog', + required: true, + )) + ->studly() + ->beforeLast('Resource') + ->trim('/') + ->trim('\\') + ->trim(' ') + ->studly() + ->replace('/', '\\'); + + if (blank($model)) { + $model = 'Resource'; + } + + if (!str($model)->contains('\\')) { + $namespace = (string) str($this->argument('namespace') ?? text( + label: 'What is the namespace of this Resource?', + placeholder: 'App\Filament\Resources', + default: 'App\Filament\Resources', + required: true, + )) + ->studly() + ->trim('/') + ->trim('\\') + ->trim(' ') + ->studly() + ->replace('/', '\\'); + + if (blank($namespace)) { + $namespace = 'App\\Filament\\Resources'; + } + } + + $modelClass = (string) str($model)->afterLast('\\'); + + $modelNamespace = str($model)->contains('\\') ? + (string) str($model)->beforeLast('\\') : + $namespace; + + $pluralModelClass = (string) str($modelClass)->pluralStudly(); + + $resource = "{$modelNamespace}\\{$modelClass}Resource"; + $resourceClass = "{$modelClass}Resource"; + + $resourcePath = (string) str($modelNamespace)->replace('\\', '/')->replace('//', '/'); + $baseResourceSourcePath = (string) str($resourceClass)->prepend('/')->prepend(base_path($resourcePath))->replace('\\', '/')->replace('//', '/')->replace('App', 'app'); + + $handlerSourceDirectory = "{$baseResourceSourcePath}/Api/Handlers/"; + $transformersSourceDirectory = "{$baseResourceSourcePath}/Api/Transformers/"; + + $namespace = text( + label: 'In which namespace would you like to create this API Docs Resource in?', + default: $virtualResourceNameSpace + ); + + $modelOfResource = $resource::getModel(); + $modelDto = new $modelOfResource(); + + $handlersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Handlers"; + $transformersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Transformers"; + + $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($virtualResourcePath)->replace('\\', '/')->replace('//', '/'); + + $handlerVirtualDirectory = "{$baseResourceVirtualPath}/Handlers/"; + $transformersVirtualDirectory = "{$baseResourceVirtualPath}/Transformers/"; + + if (method_exists($resource, 'getApiTransformer')) { + // generate API transformer + $transformers = method_exists($resource, 'apiTransformers') ? ($modelNamespace . "\\" . $resourceClass)::apiTransformers() : [($modelNamespace . "\\" . $resourceClass)::getApiTransformer()]; + foreach ($transformers as $transKey => $transformer) { + $transformerClassPath = (string) str($transformer); + $transformerClass = (string) str($transformerClassPath)->afterLast('\\'); + + $stubVars = [ + 'namespace' => $namespace, + 'modelClass' => $pluralModelClass, + 'resourceClass' => '\\' . $resourceClass . '\\Transformers', + 'transformerName' => $transformerClass, + 'transformerProperties' => '', + ]; + + if (property_exists($modelDto, 'dataClass') || method_exists($modelDto, 'dataClass')) { + $dtoProperties = $this->readModelDto($modelDto::class); + foreach ($dtoProperties as $dtoLine) { + $stubVars['transformerProperties'] .= $dtoLine . ","; + $stubVars['transformerProperties'] .= "\n "; + } + } + + if (!$this->checkForCollision(["{$transformersVirtualDirectory}/{$transformerClass}.php"])) { + $this->copyStubToApp("Api/Transformer", $transformersVirtualDirectory . '/' . $transformerClass . '.php', $stubVars); + } + } + } + + try { + $handlerMethods = File::allFiles($handlerSourceDirectory); + + foreach ($handlerMethods as $handler) { + $handlerName = basename($handler->getFileName(), '.php'); + // stub exists? + if (!$this->fileExists($this->getDefaultStubPath() . "/ApiDocs{$handlerName}")) { + + if (!$this->checkForCollision(["{$handlerVirtualDirectory}/{$handlerName}.php"])) { + + $this->copyStubToApp("Api/{$handlerName}", $handlerVirtualDirectory . '/' . $handlerName . '.php', [ + 'handlersVirtualNamespace' => $handlersVirtualNamespace, + 'transformersVirtualNamespace' => $transformersVirtualNamespace, + 'resource' => $resource, + 'resourceClass' => $resourceClass, + 'realResource' => "{$resource}\\Api\\Handlers\\{$handlerName}", + 'resourceNamespace' => $modelNamespace, + 'modelClass' => $modelClass, + 'pluralClass' => $pluralModelClass, + 'handlerClass' => $handler, + 'handlerName' => $handlerName, + 'capitalsResource' => strtoupper($modelClass), + 'path' => '/' . str($pluralModelClass)->kebab(), + ]); + } + } + } + } catch (Exception $e) { + $this->components->error($e->getMessage()); + } + + $this->components->info("Successfully created API Docs for {$resource}!"); + + return static::SUCCESS; + } + + private function readModelDto(string $model): array + { + $openAttrReflection = new ReflectionClass(OAT\Property::class); + + $typesProperty = array_values(array_filter($openAttrReflection->getProperties(), function ($v) { + return $v->getName() == '_types'; + })); + + $openApiAttrTypes = $typesProperty[0]->getValue(); + $modelReflection = new ReflectionClass($model); + + $dtoClass = $modelReflection->getProperty('dataClass')->getDefaultValue(); + $dtoReflection = new ReflectionClass($dtoClass); + + foreach ($dtoReflection->getProperties() as $property) { + if (!empty($property->getAttributes())) { + $attribute = $property->getAttributes()[0]; + + if (strpos($attribute->getName(), ApiPropertyInfo::class) !== false) { + $propertyTxt = ""; + + $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', "; + + $propertyTxt .= match ($property->getType()->getName()) { + 'array' => "items: new OAT\Items(), ", + 'object' => "schema: '{}', ", + default => '', + }; + + foreach ($attribute->getArguments() as $key => $argument) { + if (array_key_exists($key, $openApiAttrTypes) && $openApiAttrTypes[$key] !== 'string') { + $propertyTxt .= $key . ": '" . $argument . "', "; + + $propertyTxt .= match ($argument) { + 'array' => "items: new OAT\Items(), ", + 'object' => "schema: '{}', ", + default => '', + }; + } else { + $propertyTxt .= $key . ": '" . $argument . "', "; + } + } + $propertyTxt = rtrim($propertyTxt, ', '); + $propertyTxt .= ")"; + $properties[] = $propertyTxt; + } + } + } + + return $properties; + } + + private function createDirectory(string $path): void + { + $path = app_path($path); + + if (!File::isDirectory($path)) { + File::makeDirectory($path, 0755, true, true); + } + } +} diff --git a/src/Exceptions/TransformerNotFoundException.php b/src/Exceptions/TransformerNotFoundException.php new file mode 100644 index 0000000..be8fa35 --- /dev/null +++ b/src/Exceptions/TransformerNotFoundException.php @@ -0,0 +1,15 @@ +getProperty('dataClass')->getDefaultValue(); + } + return null; + } + public static function getApiTransformer(): ?string { - if (! method_exists(static::$resource, 'getApiTransformer')) { - return DefaultTransformer::class; + return match (ApiService::getApiVersionMethod()) { + 'path' => static::getTransformerFromUrlPath(), + 'query' => static::getTransformerFromUrlQuery(), + 'headers' => static::getTransformerFromRequestHeader(), + }; + } + + /** + * @return array + */ + public static function getApiTransformers(): array + { + return array_merge([ + ApiService::getDefaultTransformerName() => DefaultTransformer::class, + ], method_exists(static::$resource, 'apiTransformers') ? + array_combine( + array_map(fn($class) => Str::kebab(class_basename($class)), $transformers = array_flip(static::$resource::apiTransformers())), + array_keys($transformers) + ) : []); // @phpstan-ignore-line + } + + /** + * @throws TransformerNotFoundException + */ + protected static function getTransformerFromUrlPath(): string + { + $routeApiVersion = request()->route(ApiService::getApiVersionParameterName()); + $transformer = Str::kebab($routeApiVersion); + + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + throw new TransformerNotFoundException($transformer); + } + + return self::getApiTransformers()[$transformer]; + } + + /** + * @throws TransformerNotFoundException + */ + protected static function getTransformerFromUrlQuery(): string + { + $queryName = strtolower(ApiService::getApiVersionParameterName()); + + if (!request()->filled($queryName)) { + if (!method_exists(static::$resource, 'getApiTransformer')) { + return self::getApiTransformers()[ApiService::getDefaultTransformerName()]; + } + return static::$resource::getApiTransformer(); + } + + $transformer = request()->input($queryName); + $transformer = Str::kebab($transformer); + + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + throw new TransformerNotFoundException($transformer); } - return static::$resource::getApiTransformer(); + return self::getApiTransformers()[$transformer]; + } + /** + * @throws TransformerNotFoundException + */ + protected static function getTransformerFromRequestHeader(): string + { + $headerName = strtolower(config('api-service.route.api_transformer_header')); + if (!request()->headers->has($headerName)) { + if (!method_exists(static::$resource, 'getApiTransformer')) { + return self::getApiTransformers()[ApiService::getDefaultTransformerName()]; + } + return static::$resource::getApiTransformer(); + } + + $transformer = request()->headers->get($headerName); + $transformer = Str::kebab($transformer); + + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + throw new TransformerNotFoundException($transformer); + } + + return self::getApiTransformers()[$transformer]; + } + + public static function getKeyName(): ?string { return static::$keyName; diff --git a/src/Traits/HasHandlerTenantScope.php b/src/Traits/HasHandlerTenantScope.php index b7d1e90..d6ff7f8 100644 --- a/src/Traits/HasHandlerTenantScope.php +++ b/src/Traits/HasHandlerTenantScope.php @@ -30,7 +30,7 @@ public static function getTenantOwnershipRelationship(Model $record): Relation { $relationshipName = static::getTenantOwnershipRelationshipName(); - if (! $record->isRelation($relationshipName)) { + if (!$record->isRelation($relationshipName)) { $resourceClass = static::class; $recordClass = $record::class; @@ -43,59 +43,71 @@ public static function getTenantOwnershipRelationship(Model $record): Relation protected static function modifyTenantQuery(Builder $query, ?Model $tenant = null): Builder { - if (request()->routeIs('api.*') && Filament::hasTenancy()) { + if (request()->routeIs('api.*')) { + + $reqPanel ??= request()->route()->parameter('panel'); $tenantId ??= request()->route()->parameter('tenant'); - $tenantOwnershipRelationship = static::getTenantOwnershipRelationship($query->getModel()); - $tenantOwnershipRelationshipName = static::getTenantOwnershipRelationshipName(); - $tenantModel = app(Filament::getTenantModel()); - - if ( - ApiService::isTenancyEnabled() && - ApiService::tenancyAwareness() && - static::isScopedToTenant() && - $tenantId && - $tenant = $tenantModel::where(Filament::getCurrentPanel()->getTenantSlugAttribute() ?? $tenantModel->getKeyName(), $tenantId)->first() - ) { - if (auth()->check()) { - - $query = match (true) { - $tenantOwnershipRelationship instanceof MorphTo => $query->whereMorphedTo( - $tenantOwnershipRelationshipName, - $tenant, - ), - $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo( - $tenant, - $tenantOwnershipRelationshipName, - ), - default => $query->whereHas( - $tenantOwnershipRelationshipName, - fn (Builder $query) => $query->whereKey($tenant->getKey()), - ), - }; + foreach (Filament::getPanels() as $panel) { + if ($panel->getPath() == $reqPanel) { + Filament::setCurrentPanel($panel); + break; } } - if ( - ApiService::isTenancyEnabled() && - ! ApiService::tenancyAwareness() && - static::isScopedToTenant() - ) { - if (auth()->check()) { - - $query = match (true) { - - $tenantOwnershipRelationship instanceof MorphTo => $query - ->where($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getMorphType(), $tenantModel->getMorphClass()) - ->whereIn($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getForeignKeyName(), request()->user()->{Str::plural($tenantOwnershipRelationshipName)}->pluck($tenantModel->getKeyName())->toArray()), - $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo( - request()->user()->{Str::plural($tenantOwnershipRelationshipName)} - ), - default => $query->whereHas( - $tenantOwnershipRelationshipName, - fn (Builder $query) => $query->whereKey($tenant->getKey()), - ), - }; + if (Filament::hasTenancy()) { + + $tenantOwnershipRelationship = static::getTenantOwnershipRelationship($query->getModel()); + $tenantOwnershipRelationshipName = static::getTenantOwnershipRelationshipName(); + $tenantModel = app(Filament::getTenantModel()); + + if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + static::isScopedToTenant() && + $tenantId && + $tenant = $tenantModel::where(Filament::getCurrentPanel()->getTenantSlugAttribute() ?? $tenantModel->getKeyName(), $tenantId)->first() + ) { + if (auth()->check()) { + + $query = match (true) { + $tenantOwnershipRelationship instanceof MorphTo => $query->whereMorphedTo( + $tenantOwnershipRelationshipName, + $tenant, + ), + $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo( + $tenant, + $tenantOwnershipRelationshipName, + ), + default => $query->whereHas( + $tenantOwnershipRelationshipName, + fn (Builder $query) => $query->whereKey($tenant->getKey()), + ), + }; + } + } + + if ( + ApiService::isTenancyEnabled() && + !ApiService::tenancyAwareness() && + static::isScopedToTenant() + ) { + if (auth()->check()) { + + $query = match (true) { + + $tenantOwnershipRelationship instanceof MorphTo => $query + ->where($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getMorphType(), $tenantModel->getMorphClass()) + ->whereIn($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getForeignKeyName(), request()->user()->{Str::plural($tenantOwnershipRelationshipName)}->pluck($tenantModel->getKeyName())->toArray()), + $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo( + request()->user()->{Str::plural($tenantOwnershipRelationshipName)} + ), + default => $query->whereHas( + $tenantOwnershipRelationshipName, + fn (Builder $query) => $query->whereKey($tenant->getKey()), + ), + }; + } } } } diff --git a/stubs/Api/ApiDocsController.stub b/stubs/Api/ApiDocsController.stub new file mode 100644 index 0000000..2d1b9bd --- /dev/null +++ b/stubs/Api/ApiDocsController.stub @@ -0,0 +1,47 @@ +getPanels() as $panel) { + if ($panel->hasPlugin('api-service')) { + $panelId = $panel->getId(); + $panelPath = $panel->getPath(); + $tenantSlugAttribute = $panel->getTenantSlugAttribute(); + $panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + + $panel_enums[] = $panelPrefix; + } +} +$panel_enums = array_unique($panel_enums); + +(defined("PANEL_ENUMS")) ?: define("PANEL_ENUMS", $panel_enums); +(defined("BASE_URL")) ?: define("BASE_URL", config('app.url')); + +#[OAT\Info( + title: "{{ title }}", + version: "{{ version }}", + contact: new OAT\Contact(name: "{{ contactName }}", email: "{{ contactEmail }}"), + termsOfService: "{{ terms }}", + license: new OAT\License(name: "{{ licenseName }}", url: "{{ licenseUrl }}",), +)] + +#[OAT\Server( + description: "API Server", + url: BASE_URL . "/api/" +)] + +#[OAT\SecurityScheme( + name: "bearerAuth", + securityScheme: "bearerAuth", + type: "http", + scheme: "bearer", + description: "Enter JWT Token", + in: "header" +)] + +class ApiDocsController {} diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub new file mode 100644 index 0000000..ae4ff3e --- /dev/null +++ b/stubs/Api/CreateHandler.stub @@ -0,0 +1,173 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +defined("API_DEFAULT_TRANSFORMER_NAME") ?: define("API_DEFAULT_TRANSFORMER_NAME", config('api-service.route.default_transformer_name')); +defined("API_VERSION_PARAMETER_NAME") ?: define("API_VERSION_PARAMETER_NAME", config('api-service.route.api_version_parameter_name')); +defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('api-service.route.api_transformer_header')); + +function apiVersionPath() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", '{version}/'); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionHeaders() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", true); +} + +function apiVersionDefault() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +match (config('api-service.route.api_version_method')) { + 'path' => apiVersionPath(), + 'query' => apiVersionQuery(), + 'headers' => apiVersionHeaders(), + default => apiVersionDefault(), +}; + + +if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + app({{ resourceClass }}::class)->isScopedToTenant() +) { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", true); +} else { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", false); +} + +if ({{ handlerName }}Real::isPublic()) { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE", true); +} else { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE", false); +} + +$transformers = {{ handlerName }}Real::getApiTransformers(); +$custom_transformers = count(array_except($transformers, ['default'])); +defined("RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS", $custom_transformers ?? 0); + +defined("PANEL_PREFIX_{{ capitalsResource }}_CREATE") ?: define("PANEL_PREFIX_{{ capitalsResource }}_CREATE", $panelPrefix); +defined("BASE_URL") ?: define("BASE_URL", config('app.url')); + +#[OAT\Post( + path: "{{ path }}", + operationId: "store{{ modelClass }}", + tags: ["{{ pluralClass }}"], + summary: "Store new {{ modelClass }}", + description: "Returns inserted {{ modelClass }} data", + servers: [ + (TENANT_AWARENESS_{{ capitalsResource }}) ? + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/{tenant}", + variables: [ + new OAT\ServerVariable( + serverVariable: "panel", + description: "Select the Filament Panel", + enum: PANEL_ENUMS, + default: PANEL_ENUMS[0] + ), + new OAT\ServerVariable( + serverVariable: "tenant", + description: 'ID of the tenant', + default: "" + ), + + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/", + variables: [ + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE) ? [["bearerAuth" => []]] : null, + parameters: [ + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + + (API_VERSION_URL_QUERY) ? new OAT\Parameter( + name: API_VERSION_PARAMETER_NAME, + in: "query", + required: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + (API_VERSION_URL_HEADERS) ? new OAT\HeaderParameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: true, + allowEmptyValue: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + ], + requestBody: new OAT\RequestBody( + required: true, + content: (RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent( + oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data"), + ]) : + new OAT\JsonContent(ref: "#/components/schemas/DefaultTransformer/properties/data") + ), + responses: [ + new OAT\Response(response: 200, description: 'Operation succesful', content: new OAT\JsonContent(type: "object", properties: [ + new OAT\Property(property: "message", type: "string", example: "Successfully Inserted Resource"), + (RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data") + ]) : + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/DefaultTransformer/properties/data") + ])), + new OAT\Response(response: 400, description: 'Bad Request'), + new OAT\Response(response: 401, description: 'Unauthenticated'), + new OAT\Response(response: 403, description: 'Forbidden'), + new OAT\Response(response: 404, description: 'Resource not Found'), + ] +)] + +class {{ handlerName }} extends {{ handlerName }}Real {} diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub new file mode 100644 index 0000000..966bd99 --- /dev/null +++ b/stubs/Api/DeleteHandler.stub @@ -0,0 +1,168 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +defined("API_DEFAULT_TRANSFORMER_NAME") ?: define("API_DEFAULT_TRANSFORMER_NAME", config('api-service.route.default_transformer_name')); +defined("API_VERSION_PARAMETER_NAME") ?: define("API_VERSION_PARAMETER_NAME", config('api-service.route.api_version_parameter_name')); +defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('api-service.route.api_transformer_header')); + +function apiVersionPath() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", '{version}/'); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionHeaders() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", true); +} + +function apiVersionDefault() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +match (config('api-service.route.api_version_method')) { + 'path' => apiVersionPath(), + 'query' => apiVersionQuery(), + 'headers' => apiVersionHeaders(), + default => apiVersionDefault(), +}; + +if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + app({{ resourceClass }}::class)->isScopedToTenant() +) { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", true); +} else { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", false); +} + +if ({{ handlerName }}Real::isPublic()) { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_DELETE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_DELETE", true); +} else { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_DELETE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_DELETE", false); +} + +$transformers = {{ handlerName }}Real::getApiTransformers(); +$custom_transformers = count(array_except($transformers, ['default'])); +defined("RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS", $custom_transformers ?? 0); + +defined("PANEL_PREFIX_{{ capitalsResource }}_DELETE") ?: define("PANEL_PREFIX_{{ capitalsResource }}_DELETE", $panelPrefix); +defined("BASE_URL") ?: define("BASE_URL", config('app.url')); + +#[OAT\Delete( + path: "{{ path }}/{id}", + operationId: "delete{{ modelClass }}", + tags: ["{{ pluralClass }}"], + summary: "Delete existing {{ modelClass }}", + description: "Deletes a {{ modelClass }} record and returns no content", + servers: [ + (TENANT_AWARENESS_{{ capitalsResource }}) ? + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/{tenant}", + variables: [ + new OAT\ServerVariable( + serverVariable: "panel", + description: "Select the Filament Panel", + enum: PANEL_ENUMS, + default: PANEL_ENUMS[0] + ), + new OAT\ServerVariable( + serverVariable: "tenant", + description: 'ID of the tenant', + default: "" + ), + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/", + variables: [ + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_DELETE) ? [["bearerAuth" => []]] : null, + parameters: [ + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + new OAT\Parameter( + name: "id", + description: "{{ modelClass }} ID", + required: true, + in: "path", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + (API_VERSION_URL_QUERY) ? new OAT\Parameter( + name: API_VERSION_PARAMETER_NAME, + in: "query", + required: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + (API_VERSION_URL_HEADERS) ? new OAT\HeaderParameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: true, + allowEmptyValue: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + ], + responses: [ + new OAT\Response(response: 200, description: 'Operation succesful', content: new OAT\JsonContent(type: "object", properties: [ + new OAT\Property(property: "message", type: "string", example: "Successfully Delete Resource"), + (RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data/items") + ]) : + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/DefaultTransformer/properties/data/items") + ])), + new OAT\Response(response: 400, description: 'Bad Request'), + new OAT\Response(response: 401, description: 'Unauthenticated'), + new OAT\Response(response: 403, description: 'Forbidden'), + new OAT\Response(response: 404, description: 'Resource not Found'), + ] +)] + +class {{ handlerName }} extends {{ handlerName }}Real {} diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub new file mode 100644 index 0000000..3888501 --- /dev/null +++ b/stubs/Api/DetailHandler.stub @@ -0,0 +1,213 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +defined("API_DEFAULT_TRANSFORMER_NAME") ?: define("API_DEFAULT_TRANSFORMER_NAME", config('api-service.route.default_transformer_name')); +defined("API_VERSION_PARAMETER_NAME") ?: define("API_VERSION_PARAMETER_NAME", config('api-service.route.api_version_parameter_name')); +defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('api-service.route.api_transformer_header')); + +function apiVersionPath() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", '{version}/'); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionHeaders() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", true); +} + +function apiVersionDefault() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +match (config('api-service.route.api_version_method')) { + 'path' => apiVersionPath(), + 'query' => apiVersionQuery(), + 'headers' => apiVersionHeaders(), + default => apiVersionDefault(), +}; + + +if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + app({{ resourceClass }}::class)->isScopedToTenant() +) { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", true); +} else { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", false); +} + +if ({{ handlerName }}Real::isPublic()) { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL", true); +} else { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL", false); +} + +$resourceTransformers = []; +$transformers = {{ handlerName }}Real::getApiTransformers(); +if (!empty($transformers)) { + foreach ($transformers as $tr_key => $transformer) { + $resourceTransformers[] = $tr_key; + } + $custom_transformers = count(array_except($transformers, ['default'])); +} + +defined("RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS", $custom_transformers ?? 0); + +defined("RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMERS", $resourceTransformers); +defined("RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMER_HEADER_KEY") ?: define("RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMER_HEADER_KEY", config('api-service.route.api_transformer_header')); + +defined("PANEL_PREFIX_{{ capitalsResource }}_DETAIL") ?: define("PANEL_PREFIX_{{ capitalsResource }}_DETAIL", $panelPrefix); +defined("BASE_URL") ?: define("BASE_URL", config('app.url')); + +#[OAT\Get( + path: "{{ path }}/{id}", + operationId: "get{{ modelClass }}Detail", + tags: ["{{ pluralClass }}"], + summary: "Get detail of {{ modelClass }}", + description: "Returns detail of {{ modelClass }}", + servers: [ + (TENANT_AWARENESS_{{ capitalsResource }}) ? + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/{tenant}", + variables: [ + new OAT\ServerVariable( + serverVariable: "panel", + description: "Select the Filament Panel", + enum: PANEL_ENUMS, + default: PANEL_ENUMS[0] + ), + new OAT\ServerVariable( + serverVariable: "tenant", + description: 'ID of the tenant', + default: "" + ), + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/", + variables: [ + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL) ? [["bearerAuth" => []]] : null, + parameters: [ + + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + + (API_VERSION_URL_QUERY) ? new OAT\Parameter( + name: API_VERSION_PARAMETER_NAME, + in: "query", + required: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + (API_VERSION_URL_HEADERS) ? new OAT\HeaderParameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: true, + allowEmptyValue: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + new OAT\Parameter( + name: "id", + description: "{{ modelClass }} ID", + required: true, + in: "path", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + new OAT\Parameter( + name: "page[offset]", + description: "Pagination offset option", + required: false, + in: "query", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + new OAT\Parameter( + name: "page[limit]", + description: "Pagination limit option", + required: false, + in: "query", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + new OAT\Parameter( + name: "sort", + description: "Sorting", + required: false, + in: "query", + schema: new OAT\Schema(type: "string"), + example: "", // @OAT\Examples(example="string", value="-created,name", summary="A comma separated value"), + ), + new OAT\Parameter( + name: "include", + description: "Include Relationships", + required: false, + in: "query", + schema: new OAT\Schema(type: "string"), + example: "", // @OAT\Examples(example="string", value="order,user", summary="A comma separated value of relationships"), + ), + ], + responses: [ + new OAT\Response(response: 200, description: 'Operation succesful', content: + (RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent(oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data/items"), + ]) : + new OAT\JsonContent(type: "object", ref: "#/components/schemas/DefaultTransformer/properties/data/items") + ), + new OAT\Response(response: 400, description: 'Bad Request'), + new OAT\Response(response: 401, description: 'Unauthenticated'), + new OAT\Response(response: 403, description: 'Forbidden'), + new OAT\Response(response: 404, description: 'Resource not Found'), + ] +)] + +class {{ handlerName }} extends {{ handlerName }}Real {} diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub new file mode 100644 index 0000000..e64f485 --- /dev/null +++ b/stubs/Api/PaginationHandler.stub @@ -0,0 +1,209 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +defined("API_DEFAULT_TRANSFORMER_NAME") ?: define("API_DEFAULT_TRANSFORMER_NAME", config('api-service.route.default_transformer_name')); +defined("API_VERSION_PARAMETER_NAME") ?: define("API_VERSION_PARAMETER_NAME", config('api-service.route.api_version_parameter_name')); +defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('api-service.route.api_transformer_header')); + +function apiVersionPath() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", '{version}/'); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionHeaders() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", true); +} + +function apiVersionDefault() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +match (config('api-service.route.api_version_method')) { + 'path' => apiVersionPath(), + 'query' => apiVersionQuery(), + 'headers' => apiVersionHeaders(), + default => apiVersionDefault(), +}; + +if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + app({{ resourceClass }}::class)->isScopedToTenant() +) { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", true); +} else { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", false); +} + +if ({{ handlerName }}Real::isPublic()) { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION", true); +} else { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION", false); +} + +$resourceTransformers = []; +$transformers = {{ handlerName }}Real::getApiTransformers(); +if (!empty($transformers)) { + foreach ($transformers as $tr_key => $transformer) { + $resourceTransformers[] = $tr_key; + } + $custom_transformers = count(array_except($transformers, ['default'])); +} + +defined("RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS", $custom_transformers ?? 0); + +defined("RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMERS", $resourceTransformers); +defined("RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMER_HEADER_KEY") ?: define("RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMER_HEADER_KEY", config('api-service.route.api_transformer_header')); + +defined("PANEL_PREFIX_{{ capitalsResource }}_PAGINATION") ?: define("PANEL_PREFIX_{{ capitalsResource }}_PAGINATION", $panelPrefix); +defined("BASE_URL") ?: define("BASE_URL", config('app.url')); + +#[OAT\Get( + // path: "/" . ((TENANT_AWARENESS_{{ capitalsResource }}) ? "{tenant}/" : "") . "{{ pluralClass }}", + path: "{{ path }}", + operationId: "get{{ pluralClass }}", + tags: ["{{ pluralClass }}"], + servers: [ + (TENANT_AWARENESS_{{ capitalsResource }}) ? + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", + variables: [ + new OAT\ServerVariable( + serverVariable: "panel", + description: "Select the Filament Panel", + enum: PANEL_ENUMS, + default: PANEL_ENUMS[0] + ), + new OAT\ServerVariable( + serverVariable: "tenant", + description: 'ID of the tenant', + default: "" + ), + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . API_VERSION_URL_PATH . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + variables: [ + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) + ], + summary: "Get list of {{ pluralClass }}", + description: "Returns list of {{ pluralClass }}", + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION) ? [["bearerAuth" => []]] : null, + parameters: [ + + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(name: "tenant", description: 'ID of the tenant', schema: new OAT\Schema(type: 'integer|string'), required: true, in: "path") : null, + + (API_VERSION_URL_QUERY) ? new OAT\Parameter( + name: API_VERSION_PARAMETER_NAME, + in: "query", + required: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + (API_VERSION_URL_HEADERS) ? new OAT\HeaderParameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: true, + allowEmptyValue: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + new OAT\Parameter( + name: "page[offset]", + description: "Pagination offset option", + required: false, + in: "query", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + new OAT\Parameter( + name: "page[limit]", + description: "Pagination limit option", + required: false, + in: "query", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + new OAT\Parameter( + name: "sort", + description: "Sorting", + required: false, + in: "query", + schema: new OAT\Schema(type: "string"), + example: "", // @OAT\Examples(example="string", value="-created,name", summary="A comma separated value"), + ), + new OAT\Parameter( + name: "include", + description: "Include Relationships", + required: false, + in: "query", + schema: new OAT\Schema(type: "string"), + example: "", // @OAT\Examples(example="string", value="order,user", summary="A comma separated value of relationships"), + ), + ], + responses: [ + new OAT\Response(response: 200, description: 'Operation succesful', content: + (RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent(oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data/items"), + ]) : + new OAT\JsonContent(ref: "#/components/schemas/DefaultTransformer/properties/data/items") + ), + new OAT\Response(response: 400, description: 'Bad Request'), + new OAT\Response(response: 401, description: 'Unauthenticated'), + new OAT\Response(response: 403, description: 'Forbidden'), + new OAT\Response(response: 404, description: 'Resource not Found'), + ] +)] + + +class {{ handlerName }} extends {{ handlerName }}Real {} + diff --git a/stubs/Api/Transformer.stub b/stubs/Api/Transformer.stub new file mode 100644 index 0000000..f7d4700 --- /dev/null +++ b/stubs/Api/Transformer.stub @@ -0,0 +1,67 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +defined("API_DEFAULT_TRANSFORMER_NAME") ?: define("API_DEFAULT_TRANSFORMER_NAME", config('api-service.route.default_transformer_name')); +defined("API_VERSION_PARAMETER_NAME") ?: define("API_VERSION_PARAMETER_NAME", config('api-service.route.api_version_parameter_name')); +defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('api-service.route.api_transformer_header')); + +function apiVersionPath() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", '{version}/'); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +function apiVersionHeaders() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", true); +} + +function apiVersionDefault() { + defined("API_VERSION_URL_PATH") ?: define("API_VERSION_URL_PATH", ''); + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", false); + defined("API_VERSION_URL_HEADERS") ?: define("API_VERSION_URL_HEADERS", false); +} + +match (config('api-service.route.api_version_method')) { + 'path' => apiVersionPath(), + 'query' => apiVersionQuery(), + 'headers' => apiVersionHeaders(), + default => apiVersionDefault(), +}; + +if ( + ApiService::isTenancyEnabled() && + ApiService::tenancyAwareness() && + app({{ resourceClass }}::class)->isScopedToTenant() +) { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", true); +} else { + defined("TENANT_AWARENESS_{{ capitalsResource }}") ?: define("TENANT_AWARENESS_{{ capitalsResource }}", false); +} + +if ({{ handlerName }}Real::isPublic()) { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE", true); +} else { + defined("RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE") ?: define("RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE", false); +} + +$transformers = {{ handlerName }}Real::getApiTransformers(); +$custom_transformers = count(array_except($transformers, ['default'])); +defined("RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS", $custom_transformers ?? 0); + +defined("PANEL_PREFIX_{{ capitalsResource }}_UPDATE") ?: define("PANEL_PREFIX_{{ capitalsResource }}_UPDATE", $panelPrefix); +defined("BASE_URL") ?: define("BASE_URL", config('app.url')); + +#[OAT\Put( + path: "{{ path }}/{id}", + operationId: "update{{ modelClass }}", + tags: ["{{ pluralClass }}"], + summary: "Update existing {{ modelClass }}", + description: "Returns updated {{ modelClass }} data", + servers: [ + (TENANT_AWARENESS_{{ capitalsResource }}) ? + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", + variables: [ + new OAT\ServerVariable( + serverVariable: "panel", + description: "Select the Filament Panel", + enum: PANEL_ENUMS, + default: PANEL_ENUMS[0] + ), + new OAT\ServerVariable( + serverVariable: "tenant", + description: 'ID of the tenant', + default: "" + ), + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + variables: [ + (API_VERSION_URL_PATH != '') ? + new OAT\ServerVariable( + serverVariable: "version", + description: 'Version or Transformer name of this API endpoint', + default: API_DEFAULT_TRANSFORMER_NAME, + ) + : null, + ] + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE) ? [["bearerAuth" => []]] : null, + parameters: [ + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + + (API_VERSION_URL_QUERY) ? new OAT\Parameter( + name: API_VERSION_PARAMETER_NAME, + in: "query", + required: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + (API_VERSION_URL_HEADERS) ? new OAT\HeaderParameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: true, + allowEmptyValue: true, + description: "Set the correct API Version / Transformer you want to use.", + schema: new OAT\Schema(type: "string") + ) : null, + + new OAT\Parameter( + name: "id", + description: "{{ modelClass }} id", + required: true, + in: "path", + schema: new OAT\Schema(type: "integer"), + example: "", // OAT\Examples(example="int", value="0", summary="An int value."), + ), + ], + requestBody: new OAT\RequestBody( + required: true, + content: new OAT\MediaType( + mediaType: "application/json", + schema: (RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Schema( + oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data/items"), + ]) : + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data/items") + ) + ), + responses: [ + new OAT\Response(response: 200, description: 'Operation succesful', content: + new OAT\JsonContent(type: "object", properties: [ + new OAT\Property(property: "message", type: "string", example: "Successfully Updated Resource"), + (RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", oneOf: [ + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data"), + new OAT\Schema(ref: "#/components/schemas/DefaultTransformer/properties/data") + ]) : + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/DefaultTransformer/properties/data") + ])), + new OAT\Response(response: 400, description: 'Bad Request'), + new OAT\Response(response: 401, description: 'Unauthenticated'), + new OAT\Response(response: 403, description: 'Forbidden'), + new OAT\Response(response: 404, description: 'Resource not Found'), + ] +)] + +class {{ handlerName }} extends {{ handlerName }}Real {} diff --git a/stubs/CreateHandler.stub b/stubs/CreateHandler.stub index 6e35f37..8746b65 100644 --- a/stubs/CreateHandler.stub +++ b/stubs/CreateHandler.stub @@ -22,10 +22,16 @@ class CreateHandler extends Handlers { { $model = new (static::getModel()); - $model->fill($request->all()); + $dtoModel = static::getDto(); + + if (!empty($dtoModel)) { + $model->fill($dtoModel::from($request)->all()); + } else { + $model->fill($request->all()); + } $model->save(); return static::sendSuccessResponse($model, "Successfully Create Resource"); } -} \ No newline at end of file +} diff --git a/stubs/CustomHandler.stub b/stubs/CustomHandler.stub index 40c5128..5917d65 100644 --- a/stubs/CustomHandler.stub +++ b/stubs/CustomHandler.stub @@ -22,10 +22,16 @@ class {{ handlerClass }} extends Handlers { { $model = new (static::getModel()); - $model->fill($request->all()); + $dtoModel = static::getDto(); + + if (!empty($dtoModel)) { + $model->fill($dtoModel::from($request)->all()); + } else { + $model->fill($request->all()); + } $model->save(); return static::sendSuccessResponse($model, "Successfully Create Resource"); } -} \ No newline at end of file +} diff --git a/stubs/DetailHandler.stub b/stubs/DetailHandler.stub index aabd6f3..bc9bc41 100644 --- a/stubs/DetailHandler.stub +++ b/stubs/DetailHandler.stub @@ -17,7 +17,7 @@ class DetailHandler extends Handlers public function handler(Request $request) { $id = $request->route('id'); - + $query = static::getEloquentQuery(); $query = QueryBuilder::for( @@ -29,6 +29,12 @@ class DetailHandler extends Handlers $transformer = static::getApiTransformer(); + $dtoModel = static::getDto(); + + if (!empty($dtoModel)) { + return new $transformer($dtoModel::from($query)); + } return new $transformer($query); + } } diff --git a/stubs/PaginationHandler.stub b/stubs/PaginationHandler.stub index 62951db..24a410e 100644 --- a/stubs/PaginationHandler.stub +++ b/stubs/PaginationHandler.stub @@ -24,6 +24,11 @@ class PaginationHandler extends Handlers { ->paginate(request()->query('per_page')) ->appends(request()->query()); + $dtoModel = static::getDto(); + + if (!empty($dtoModel)) { + return static::getApiTransformer()::collection($dtoModel::collect($query)); + } return static::getApiTransformer()::collection($query); } } diff --git a/stubs/UpdateHandler.stub b/stubs/UpdateHandler.stub index 7dad4c3..1037540 100644 --- a/stubs/UpdateHandler.stub +++ b/stubs/UpdateHandler.stub @@ -28,8 +28,15 @@ class UpdateHandler extends Handlers { $model->fill($request->all()); + $dtoModel = static::getDto(); + + if (!empty($dtoModel)) { + $dtoModelData = static::getDto()::validateAndCreate($model); + $model->fill($dtoModelData->all()); + } + $model->save(); return static::sendSuccessResponse($model, "Successfully Update Resource"); } -} \ No newline at end of file +}