From ababe8cd0bfe0613a377f0dfb155410cbb349b0e Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:05:24 +0200 Subject: [PATCH 01/63] first work on api docs generation with command and stubs --- src/Commands/MakeApiDocsCommand.php | 216 ++++++++++++++++++++++++++++ stubs/Api/ApiDocsController.stub | 49 +++++++ stubs/Api/CreateHandler.stub | 89 ++++++++++++ stubs/Api/DeleteHandler.stub | 93 ++++++++++++ stubs/Api/DetailHandler.stub | 122 ++++++++++++++++ stubs/Api/PaginationHandler.stub | 121 ++++++++++++++++ stubs/Api/Transformer.stub | 67 +++++++++ stubs/Api/UpdateHandler.stub | 101 +++++++++++++ 8 files changed, 858 insertions(+) create mode 100644 src/Commands/MakeApiDocsCommand.php create mode 100644 stubs/Api/ApiDocsController.stub create mode 100644 stubs/Api/CreateHandler.stub create mode 100644 stubs/Api/DeleteHandler.stub create mode 100644 stubs/Api/DetailHandler.stub create mode 100644 stubs/Api/PaginationHandler.stub create mode 100644 stubs/Api/Transformer.stub create mode 100644 stubs/Api/UpdateHandler.stub diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php new file mode 100644 index 0000000..3f4b7e1 --- /dev/null +++ b/src/Commands/MakeApiDocsCommand.php @@ -0,0 +1,216 @@ +checkForCollision([$serverPath . '/' . $serverFile]); + + 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', $serverPath . '/' . $serverFile, [ + 'namespace' => $serverNameSpace, + 'title' => $serverTitle, + 'version' => $serverVersion, + 'contactName' => $serverContactName, + 'contactEmail' => $serverContactEmail, + 'terms' => $serverTerms, + 'licenseName' => 'MIT License', + 'licenseUrl' => 'https://opensource.org/license/mit', + ]); + } + + $panel = Filament::getDefaultPanel(); + + $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('//', '/'); + + $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: $serverNameSpace + ); + + $handlersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Handlers"; + $transformersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Transformers"; + + $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($serverPath)->replace('\\', '/')->replace('//', '/'); + + $handlerVirtualDirectory = "{$baseResourceVirtualPath}/Handlers/"; + $transformersVirtualDirectory = "{$baseResourceVirtualPath}/Transformers/"; + + if (method_exists($resource, 'getApiTransformer')) { + // generate API transformer + $transformer = ($modelNamespace . "\\" . $resourceClass)::getApiTransformer(); + $transformerClassPath = (string) str($transformer); + $transformerClass = (string) str($transformerClassPath)->afterLast('\\'); + + $stubVars = [ + 'namespace' => $namespace, + 'modelClass' => $pluralModelClass, + 'transformerName' => $transformerClass, + ]; + + 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' => '/' . strtolower($pluralModelClass), + ]); + } + } + } + } catch (Exception $e) { + $this->components->error($e->getMessage()); + } + + $this->components->info("Successfully created API Docs for {$resource}!"); + + return static::SUCCESS; + } + + + private function createDirectory(string $path): void + { + $path = app_path($path); + + if (!File::isDirectory($path)) { + File::makeDirectory($path, 0755, true, true); + } + } +} diff --git a/stubs/Api/ApiDocsController.stub b/stubs/Api/ApiDocsController.stub new file mode 100644 index 0000000..08e38d9 --- /dev/null +++ b/stubs/Api/ApiDocsController.stub @@ -0,0 +1,49 @@ +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..2277881 --- /dev/null +++ b/stubs/Api/CreateHandler.stub @@ -0,0 +1,89 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +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); +} + +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/{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: "" + ), + + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/", + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_CREATE) ? [["bearerAuth" => []]] : null, + parameters: [ + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + ], + requestBody: new OAT\RequestBody( + required: true, + content: new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer/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"), + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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..85aa2e3 --- /dev/null +++ b/stubs/Api/DeleteHandler.stub @@ -0,0 +1,93 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +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); +} + +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/{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: "" + ), + + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/", + ) + ], + 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."), + ), + ], + 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"), + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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..1b95169 --- /dev/null +++ b/stubs/Api/DetailHandler.stub @@ -0,0 +1,122 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +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); +} + +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/{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: "" + ), + + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/", + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL) ? [["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."), + ), + 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: new OAT\JsonContent(type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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..58175fa --- /dev/null +++ b/stubs/Api/PaginationHandler.stub @@ -0,0 +1,121 @@ +getCurrentPanel(); + +$panelId = $panel->getId(); +$panelPath = $panel->getPath(); +$hasTenancy = $panel->hasTenancy(); +$tenantSlugAttribute = $panel->getTenantSlugAttribute(); +$panelPrefix = app(ApiService::class)->isRoutePrefixedByPanel() ? $panelPath ?? $panelId : ''; + +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); +} + +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/{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: "" + ), + + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + ) + ], + 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, + 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: new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer")), + 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..d7cbfa5 --- /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 : ''; + +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); +} + +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/{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: "" + ), + + ] + ) : + new OAT\Server( + description: "API Server Filament Panel", + url: BASE_URL . "/api/" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + ) + ], + security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_UPDATE) ? [["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."), + ), + ], + requestBody: new OAT\RequestBody( + required: true, + content: new OAT\MediaType( + mediaType: "application/json", + schema: new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/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"), + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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 {} From 158825483b2be08c4df76a8c828ae42c8a85be24 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:32:42 +0200 Subject: [PATCH 02/63] basic explanation for Swagger Api Docs generation --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 37f40d8..5e60b5e 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,32 @@ 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: + +``` +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`. + +Then you can use the following command to generate API docs for your resources: + +```bash +php artisan make:filament-api-docs {resource?} {namespace?} +``` + +The CLI command accepts two optional parameters: `{resource?}` and `{namespace?}`. +By default the Swagger API Docs will be placed in `App\Virtual\Filament\Resources`. + +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`. + ## License The MIT License (MIT). From 7f6c761dd303a517ab6ffc8283dc71d181aa6bd9 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:35:38 +0200 Subject: [PATCH 03/63] more updated readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e60b5e..e90312f 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,9 @@ php artisan make:filament-api-docs {resource?} {namespace?} ``` The CLI command accepts two optional parameters: `{resource?}` and `{namespace?}`. -By default the Swagger API Docs will be placed in `App\Virtual\Filament\Resources`. +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. From ae2504d9a1330b2a4a1ca12fa06f1e3d429fd341 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:38:08 +0200 Subject: [PATCH 04/63] removed unused $panel parameter --- src/Commands/MakeApiDocsCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 3f4b7e1..23ab271 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -83,8 +83,6 @@ public function handle(): int ]); } - $panel = Filament::getDefaultPanel(); - $model = (string) str($this->argument('resource') ?? text( label: 'What is the Resource name?', placeholder: 'Blog', From 8251ece81adecd83f9031ead71e10e9d1c240df0 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:51:18 +0200 Subject: [PATCH 05/63] wrong namespace --- src/Commands/MakeApiDocsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 23ab271..cd4d719 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -1,6 +1,6 @@ Date: Wed, 3 Apr 2024 14:52:36 +0200 Subject: [PATCH 06/63] cleanup unused classes --- src/Commands/MakeApiDocsCommand.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index cd4d719..c542e96 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -3,15 +3,10 @@ namespace Rupadana\ApiService\Commands; use Exception; -use Filament\Facades\Filament; -use Filament\Panel; use Filament\Support\Commands\Concerns\CanManipulateFiles; use Illuminate\Console\Command; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\File; -use ReflectionMethod; -use function Laravel\Prompts\select; use function Laravel\Prompts\text; class MakeApiDocsCommand extends Command From 8159fb57da6d47715634188880b47c9ab8fb6b50 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 14:55:54 +0200 Subject: [PATCH 07/63] small changes cleanup --- src/Commands/MakeApiDocsCommand.php | 1 - stubs/Api/ApiDocsController.stub | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index c542e96..f0abccc 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -197,7 +197,6 @@ public function handle(): int return static::SUCCESS; } - private function createDirectory(string $path): void { $path = app_path($path); diff --git a/stubs/Api/ApiDocsController.stub b/stubs/Api/ApiDocsController.stub index 08e38d9..2d1b9bd 100644 --- a/stubs/Api/ApiDocsController.stub +++ b/stubs/Api/ApiDocsController.stub @@ -6,6 +6,7 @@ use OpenApi\Attributes as OAT; use Rupadana\ApiService\ApiService; $panel_enums = []; + foreach (filament()->getPanels() as $panel) { if ($panel->hasPlugin('api-service')) { $panelId = $panel->getId(); @@ -17,6 +18,7 @@ foreach (filament()->getPanels() as $panel) { } } $panel_enums = array_unique($panel_enums); + (defined("PANEL_ENUMS")) ?: define("PANEL_ENUMS", $panel_enums); (defined("BASE_URL")) ?: define("BASE_URL", config('app.url')); @@ -33,7 +35,6 @@ $panel_enums = array_unique($panel_enums); url: BASE_URL . "/api/" )] - #[OAT\SecurityScheme( name: "bearerAuth", securityScheme: "bearerAuth", @@ -43,7 +44,4 @@ $panel_enums = array_unique($panel_enums); in: "header" )] -class ApiDocsController -{ - -} +class ApiDocsController {} From 0f81ae7366a83839a44eb25ffbf085254eaefc75 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 15:07:37 +0200 Subject: [PATCH 08/63] add command to ApiServiceProvider getCommands() function --- src/ApiServiceServiceProvider.php | 2 ++ 1 file changed, 2 insertions(+) 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, ]; } From 74cbcfe797afb12afc6ae74ad4807847c631154e Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 3 Apr 2024 15:10:24 +0200 Subject: [PATCH 09/63] added correct namespace for Transformer stub --- src/Commands/MakeApiDocsCommand.php | 1 + stubs/Api/Transformer.stub | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index f0abccc..cb2193f 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -153,6 +153,7 @@ public function handle(): int $stubVars = [ 'namespace' => $namespace, 'modelClass' => $pluralModelClass, + 'resourceClass' => $resourceClass, 'transformerName' => $transformerClass, ]; diff --git a/stubs/Api/Transformer.stub b/stubs/Api/Transformer.stub index d7cbfa5..1524bbf 100644 --- a/stubs/Api/Transformer.stub +++ b/stubs/Api/Transformer.stub @@ -1,6 +1,6 @@ Date: Thu, 9 May 2024 11:10:04 +0200 Subject: [PATCH 10/63] fixes from @POMXARK, OAT/Xml and replace App to app. --- src/Commands/MakeApiDocsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index cb2193f..05d20a1 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -126,7 +126,7 @@ public function handle(): int $resourceClass = "{$modelClass}Resource"; $resourcePath = (string) str($modelNamespace)->replace('\\', '/')->replace('//', '/'); - $baseResourceSourcePath = (string) str($resourceClass)->prepend('/')->prepend(base_path($resourcePath))->replace('\\', '/')->replace('//', '/'); + $baseResourceSourcePath = (string) str($resourceClass)->prepend('/')->prepend(base_path($resourcePath))->replace('\\', '/')->replace('//', '/')->replace('App', 'app'); $handlerSourceDirectory = "{$baseResourceSourcePath}/Api/Handlers/"; $transformersSourceDirectory = "{$baseResourceSourcePath}/Api/Transformers/"; From 79bf39c5c88eb54ffafb34cde393b2ba6d032077 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 9 May 2024 11:11:34 +0200 Subject: [PATCH 11/63] fixes from @POMXARK, OAT/Xml and replace App to app. --- README.md | 18 ++++++++++++++++-- stubs/Api/Transformer.stub | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e90312f..083cd69 100644 --- a/README.md +++ b/README.md @@ -169,8 +169,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; } @@ -278,6 +280,12 @@ Then you can use the following command to generate API docs for your resources: 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. @@ -289,6 +297,12 @@ Regenerating an API Docs Serverinfo or Resource will always ask you if you want 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 manually generate the Api Docs manually because in you `l5-swagger.php` config you have set `defatuls.generate_always` to `false` you can do so by invoking: + +```bash +php artisan l5-swagger:generate +``` + ## License The MIT License (MIT). diff --git a/stubs/Api/Transformer.stub b/stubs/Api/Transformer.stub index 1524bbf..ffc7de6 100644 --- a/stubs/Api/Transformer.stub +++ b/stubs/Api/Transformer.stub @@ -9,7 +9,7 @@ use OpenApi\Attributes as OAT; schema: "{{ transformerName }}", title: "{{ transformerName }}", description: "{{ modelClass }} API Transformer", - xml: new OAT\XML(name: "{{ transformerName }}"), + xml: new OAT\Xml(name: "{{ transformerName }}"), )] class {{ transformerName }} { From bf0dc5cc9be7618f4d500e51d470eb470e3a204a Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 9 May 2024 11:13:07 +0200 Subject: [PATCH 12/63] small readme change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 083cd69..925c0e7 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ Regenerating an API Docs Serverinfo or Resource will always ask you if you want 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 manually generate the Api Docs manually because in you `l5-swagger.php` config you have set `defatuls.generate_always` to `false` you can do so by invoking: +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 From fa0df1f8e4434f5d2f16f1d545fac07381a00a59 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 09:55:06 +0200 Subject: [PATCH 13/63] use str()->kebab() for path. --- src/Commands/MakeApiDocsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 05d20a1..d262c31 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -184,7 +184,7 @@ public function handle(): int 'handlerClass' => $handler, 'handlerName' => $handlerName, 'capitalsResource' => strtoupper($modelClass), - 'path' => '/' . strtolower($pluralModelClass), + 'path' => '/' . str($pluralModelClass)->kebab(), ]); } } From e13db06c027a8f4065b23b9b32393847a87f2816 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:01:57 +0200 Subject: [PATCH 14/63] updated README for required transformer when using API Generator --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 925c0e7..b4d4946 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,10 @@ 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 an Transformer, which is explained above at the section "[Transform API Resonse](#transform-api-response)". + Then you can use the following command to generate API docs for your resources: ```bash From d79afaef0ddfa57ee3182cfe66da7d8a0b171ae8 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:14:10 +0200 Subject: [PATCH 15/63] better Transformer API Generator readme --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b4d4946..56179b5 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ In development we recommend setting the config in `l5-swagger.php` `defaults.gen 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 an Transformer, which is explained above at the section "[Transform API Resonse](#transform-api-response)". +Therefor you should always create an Transformer, which is explained above at the section "[Transform API Resonse](#transform-api-response)". Then you can use the following command to generate API docs for your resources: @@ -307,6 +307,34 @@ If you want to generate the Api Docs manually because in your `l5-swagger.php` c 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: + +``` + 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 https://zircote.github.io/swagger-php/reference/attributes.html#property + + ## License The MIT License (MIT). From 161f21e7b164097fe5e16923e768955bb024fd13 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:14:24 +0200 Subject: [PATCH 16/63] readme php syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56179b5..059ba45 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ After you have generated the Swagger API Docs, you can add your required Transfo by default as an example it will generate this when you use a BlogTransformer: -``` +```php class BlogTransformer { #[OAT\Property( From af32378421e71ea9933c87ede74df5ba345a65bd Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:15:09 +0200 Subject: [PATCH 17/63] format readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 059ba45..921610a 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ After you have generated the Swagger API Docs, you can add your required Transfo by default as an example it will generate this when you use a BlogTransformer: ```php - class BlogTransformer { +class BlogTransformer { #[OAT\Property( property: "data", From 47246bb99dc943f4385b74d574f67213404068ce Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:15:47 +0200 Subject: [PATCH 18/63] format readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 921610a..9f39f16 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ class BlogTransformer { 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 ] ), @@ -334,7 +334,6 @@ class BlogTransformer { You can find more about all possible properties at https://zircote.github.io/swagger-php/reference/attributes.html#property - ## License The MIT License (MIT). From 8bfafbd232c9aa10a924c4a30ae1054ecca57d14 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 24 May 2024 10:16:47 +0200 Subject: [PATCH 19/63] typo readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f39f16..37ecd4a 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ In development we recommend setting the config in `l5-swagger.php` `defaults.gen 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 an Transformer, which is explained above at the section "[Transform API Resonse](#transform-api-response)". +Therefor you should always create a Transformer, which is explained above at the section [Transform API Resonse](#transform-api-response). Then you can use the following command to generate API docs for your resources: From 596291ec100cf745e2d1c41e1afbcffa37b698ba Mon Sep 17 00:00:00 2001 From: Rupadana <34137674+rupadana@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:25:19 +0700 Subject: [PATCH 20/63] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37ecd4a..9a5a2ee 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ In development we recommend setting the config in `l5-swagger.php` `defaults.gen 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 Resonse](#transform-api-response). +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: From c27d69fc4778610f9ce64a3a9c5b25f4f0c8db9a Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 7 Jun 2024 12:59:45 +0200 Subject: [PATCH 21/63] Fixes some situations where default Panel is not an Tenancy enabled panel. now it gets the panel from URL and then checks if that panel has tenancy enabled. --- src/Traits/HasHandlerTenantScope.php | 112 +++++++++++++++------------ 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/src/Traits/HasHandlerTenantScope.php b/src/Traits/HasHandlerTenantScope.php index 766d924..be0c3b6 100644 --- a/src/Traits/HasHandlerTenantScope.php +++ b/src/Traits/HasHandlerTenantScope.php @@ -30,7 +30,7 @@ protected static function getTenantOwnershipRelationship(Model $record): Relatio { $relationshipName = static::getTenantOwnershipRelationshipName(); - if (! $record->isRelation($relationshipName)) { + if (!$record->isRelation($relationshipName)) { $resourceClass = static::class; $recordClass = $record::class; @@ -44,60 +44,72 @@ protected static function getTenantOwnershipRelationship(Model $record): Relatio 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($tenantModel->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($tenantModel->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($tenantModel->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($tenantModel->getKey()), + ), + }; + } } } } From b14fad5ab67c90f1a037dd0bd361ac752430d22b Mon Sep 17 00:00:00 2001 From: Eelco Date: Tue, 11 Jun 2024 11:01:09 +0200 Subject: [PATCH 22/63] base codework for multiple transformers via HTTP Headers --- config/api-service.php | 5 ++- .../TransformerNotFoundException.php | 15 ++++++++ src/Http/Handlers.php | 37 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/Exceptions/TransformerNotFoundException.php diff --git a/config/api-service.php b/config/api-service.php index fdb5862..9424792 100644 --- a/config/api-service.php +++ b/config/api-service.php @@ -1,5 +1,7 @@ [ 'token' => [ @@ -16,9 +18,10 @@ 'route' => [ 'panel_prefix' => true, 'use_resource_middlewares' => false, + 'api_transformer_header' => env('API_TRANSFORMER_HEADER', 'X-API-TRANSFORMER'), ], 'tenancy' => [ 'enabled' => false, 'awareness' => false, - ], + ] ]; 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 @@ + + */ + protected static function getApiTransformers(): array + { + return array_merge([ + 'default' => DefaultTransformer::class, + ], method_exists(static::$resource, 'apiTransformers') ? array_combine( + array_map(fn ($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), + $transformers + ) : []); // @phpstan-ignore-line + } + + /** + * @throws Exception + */ + protected static function getTransformerFromRequestHeader(): string + { + $headerName = config('api-service.route.api_transformer_header'); + $headerName = strtolower($headerName); + if (!request()->headers->has($headerName)) { + return self::getApiTransformers()['default']; + } + + $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; From 5885fa4ae0dd0ad379bc8ee415cf008d0bb16a7b Mon Sep 17 00:00:00 2001 From: Eelco Date: Tue, 11 Jun 2024 11:18:06 +0200 Subject: [PATCH 23/63] Added updated readme for multiple handlers, and fallback to default handler in resource when no header specified --- README.md | 54 +++++++++++++++++++++++++++++++++++++++++++ src/Http/Handlers.php | 11 ++++----- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9a5a2ee..300d051 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,60 @@ 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 + [ + BlogTransformer::class, + ModifiedBlogTransformer::class, + 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. + ### Group Name & Prefix You can edit prefix & group route name as you want, default this plugin use model singular label; diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 6f2dee0..7089837 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -97,11 +97,7 @@ public static function getModel() public static function getApiTransformer(): ?string { - if (! method_exists(static::$resource, 'getApiTransformer')) { - return DefaultTransformer::class; - } - - return static::$resource::getApiTransformer(); + return static::getTransformerFromRequestHeader(); } /** @@ -125,7 +121,10 @@ protected static function getTransformerFromRequestHeader(): string $headerName = config('api-service.route.api_transformer_header'); $headerName = strtolower($headerName); if (!request()->headers->has($headerName)) { - return self::getApiTransformers()['default']; + if (!method_exists(static::$resource, 'getApiTransformer')) { + return self::getApiTransformers()['default']; + } + return static::$resource::getApiTransformer(); } $transformer = request()->headers->get($headerName); From 82a1633ac69cb7e4e9521a95de9be4d353b0dd18 Mon Sep 17 00:00:00 2001 From: Eelco Date: Tue, 11 Jun 2024 12:00:29 +0200 Subject: [PATCH 24/63] merge commits --- README.md | 1 + config/api-service.php | 1 + src/Resources/TokenResource.php | 5 +++++ src/Traits/HasHandlerTenantScope.php | 9 ++++----- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 300d051..8673b41 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ php artisan vendor:publish --tag=api-service-config return [ 'navigation' => [ 'token' => [ + 'cluster' => null, 'group' => 'User', 'sort' => -1, 'icon' => 'heroicon-o-key' diff --git a/config/api-service.php b/config/api-service.php index 9424792..1d473ad 100644 --- a/config/api-service.php +++ b/config/api-service.php @@ -5,6 +5,7 @@ return [ 'navigation' => [ 'token' => [ + 'cluster' => null, 'group' => 'User', 'sort' => -1, 'icon' => 'heroicon-o-key', diff --git a/src/Resources/TokenResource.php b/src/Resources/TokenResource.php index a463f3b..9c0c913 100644 --- a/src/Resources/TokenResource.php +++ b/src/Resources/TokenResource.php @@ -131,6 +131,11 @@ public static function getPages(): array ]; } + public static function getCluster(): ?string + { + return config('api-service.navigation.token.cluster', null); + } + public static function getNavigationGroup(): ?string { return config('api-service.navigation.token.group') ?? config('api-service.navigation.group.token'); diff --git a/src/Traits/HasHandlerTenantScope.php b/src/Traits/HasHandlerTenantScope.php index be0c3b6..5233008 100644 --- a/src/Traits/HasHandlerTenantScope.php +++ b/src/Traits/HasHandlerTenantScope.php @@ -16,7 +16,7 @@ trait HasHandlerTenantScope { protected static ?string $tenantOwnershipRelationshipName = null; - protected static function getTenantOwnershipRelationshipName(): string + public static function getTenantOwnershipRelationshipName(): string { return static::$tenantOwnershipRelationshipName ?? Filament::getTenantOwnershipRelationshipName(); } @@ -26,7 +26,7 @@ protected static function getOwnerRelationshipId() return static::getOwnerRelationshipName() . '_' . app(static::getModel())->getKeyName(); } - protected static function getTenantOwnershipRelationship(Model $record): Relation + public static function getTenantOwnershipRelationship(Model $record): Relation { $relationshipName = static::getTenantOwnershipRelationshipName(); @@ -43,7 +43,6 @@ protected static function getTenantOwnershipRelationship(Model $record): Relatio protected static function modifyTenantQuery(Builder $query, ?Model $tenant = null): Builder { - if (request()->routeIs('api.*')) { $reqPanel ??= request()->route()->parameter('panel'); @@ -82,7 +81,7 @@ protected static function modifyTenantQuery(Builder $query, ?Model $tenant = nul ), default => $query->whereHas( $tenantOwnershipRelationshipName, - fn (Builder $query) => $query->whereKey($tenantModel->getKey()), + fn (Builder $query) => $query->whereKey($tenant->getKey()), ), }; } @@ -106,7 +105,7 @@ protected static function modifyTenantQuery(Builder $query, ?Model $tenant = nul ), default => $query->whereHas( $tenantOwnershipRelationshipName, - fn (Builder $query) => $query->whereKey($tenantModel->getKey()), + fn (Builder $query) => $query->whereKey($tenant->getKey()), ), }; } From abd2d589e9cc24fe23ea3f879ae8f46ccd21ee8b Mon Sep 17 00:00:00 2001 From: Eelco Date: Tue, 11 Jun 2024 12:32:40 +0200 Subject: [PATCH 25/63] reverted the missing trait in handlers --- src/Http/Handlers.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 7089837..ae322a3 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -10,12 +10,14 @@ use Illuminate\Support\Str; use Rupadana\ApiService\ApiService; use Rupadana\ApiService\Exceptions\TransformerNotFoundException; +use Rupadana\ApiService\Traits\HasHandlerTenantScope; use Rupadana\ApiService\Traits\HttpResponse; use Rupadana\ApiService\Transformers\DefaultTransformer; class Handlers { use HttpResponse; + use HasHandlerTenantScope; protected Panel $panel; public static ?string $uri = '/'; public static string $method = 'get'; From 799706d9284dadd525078f55c1e39282d6344b2a Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 14:53:36 +0200 Subject: [PATCH 26/63] ability to select the created custom Transformers in the api Docs --- src/Commands/MakeApiDocsCommand.php | 26 +++++++++++++++++++------- stubs/Api/DetailHandler.stub | 10 ++++++++++ stubs/Api/PaginationHandler.stub | 8 ++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index d262c31..08a89d1 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -18,13 +18,16 @@ class MakeApiDocsCommand extends Command public function handle(): int { - // ApiDocsController exists? - $serverPath = app_path('Virtual/Filament/Resources'); - $serverNameSpace = 'App\\Virtual\\Filament\\Resources'; + $baseServerPath = app_path('Virtual/'); + $serverNameSpace = 'App\\Virtual'; $serverFile = 'ApiDocsController.php'; - $isNotInstalled = $this->checkForCollision([$serverPath . '/' . $serverFile]); + $resourcePath = $baseServerPath . 'Filament/Resources'; + $resourceNameSpace = $serverNameSpace . '\\Filament\\Resources'; + + $isNotInstalled = $this->checkForCollision([$baseServerPath . '/' . $serverFile]); + // ApiDocsController exists? if (!$isNotInstalled) { $this->components->info("Please provide basic API Docs information."); @@ -66,7 +69,7 @@ public function handle(): int $this->createDirectory('Virtual/Filament/Resources'); - $this->copyStubToApp('Api/ApiDocsController', $serverPath . '/' . $serverFile, [ + $this->copyStubToApp('ApiDocsController', $baseServerPath . '/' . $serverFile, [ 'namespace' => $serverNameSpace, 'title' => $serverTitle, 'version' => $serverVersion, @@ -133,13 +136,13 @@ public function handle(): int $namespace = text( label: 'In which namespace would you like to create this API Docs Resource in?', - default: $serverNameSpace + default: $resourceNameSpace ); $handlersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Handlers"; $transformersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Transformers"; - $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($serverPath)->replace('\\', '/')->replace('//', '/'); + $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($resourcePath)->replace('\\', '/')->replace('//', '/'); $handlerVirtualDirectory = "{$baseResourceVirtualPath}/Handlers/"; $transformersVirtualDirectory = "{$baseResourceVirtualPath}/Transformers/"; @@ -162,6 +165,14 @@ public function handle(): int } } + $resourceTransformers = []; + if (method_exists($resource, 'apiTransformers')) { + $transformers = ($modelNamespace . "\\" . $resourceClass)::apiTransformers(); + foreach($transformers as $transformer) { + $resourceTransformers[] = str($transformer)->afterLast('\\')->kebab(); + } + } + try { $handlerMethods = File::allFiles($handlerSourceDirectory); @@ -184,6 +195,7 @@ public function handle(): int 'handlerClass' => $handler, 'handlerName' => $handlerName, 'capitalsResource' => strtoupper($modelClass), + 'resourceTransformers' => $resourceTransformers, 'path' => '/' . str($pluralModelClass)->kebab(), ]); } diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index 1b95169..38868c0 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -68,7 +68,17 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); ], security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL) ? [["bearerAuth" => []]] : null, parameters: [ + + new OAT\HeaderParameter( + name: "{{ config('api-service.route.api_transformer_header') }}", + description: "Select a custom Transformer if needed", + in: "header", + allowEmptyValue: true, + schema: new OAT\Schema(enum: {{ resourceTransformers }}), + ), + // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, + new OAT\Parameter( name: "id", description: "{{ modelClass }} ID", diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 58175fa..25096d4 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -71,9 +71,17 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION) ? [["bearerAuth" => []]] : null, parameters: [ + new OAT\HeaderParameter( + name: "{{ config('api-service.route.api_transformer_header') }}", + description: "Select a custom Transformer if needed", + in: "header", + allowEmptyValue: true, + schema: new OAT\Schema(enum: {{ resourceTransformers }}), + ), // (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, + new OAT\Parameter( name: "page[offset]", description: "Pagination offset option", From a99eeb60fdbf58e0632eb03e17afc08b5b4f490a Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 15:13:44 +0200 Subject: [PATCH 27/63] small fix in duplicate overridden variable --- src/Commands/MakeApiDocsCommand.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 08a89d1..ed08046 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -18,12 +18,12 @@ class MakeApiDocsCommand extends Command public function handle(): int { - $baseServerPath = app_path('Virtual/'); + $baseServerPath = app_path('Virtual'); $serverNameSpace = 'App\\Virtual'; $serverFile = 'ApiDocsController.php'; - $resourcePath = $baseServerPath . 'Filament/Resources'; - $resourceNameSpace = $serverNameSpace . '\\Filament\\Resources'; + $virtualResourcePath = $baseServerPath . 'Filament/Resources'; + $virtualResourceNameSpace = $serverNameSpace . '\\Filament\\Resources'; $isNotInstalled = $this->checkForCollision([$baseServerPath . '/' . $serverFile]); @@ -136,13 +136,13 @@ public function handle(): int $namespace = text( label: 'In which namespace would you like to create this API Docs Resource in?', - default: $resourceNameSpace + default: $virtualResourceNameSpace ); $handlersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Handlers"; $transformersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Transformers"; - $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($resourcePath)->replace('\\', '/')->replace('//', '/'); + $baseResourceVirtualPath = (string) str($resourceClass)->prepend('/')->prepend($virtualResourcePath)->replace('\\', '/')->replace('//', '/'); $handlerVirtualDirectory = "{$baseResourceVirtualPath}/Handlers/"; $transformersVirtualDirectory = "{$baseResourceVirtualPath}/Transformers/"; From 355bc14de7b9850aa1ff1682d1aeaba25825b8f8 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 15:55:05 +0200 Subject: [PATCH 28/63] detail and pagination handler now retreive all the Transformers, and uses the transformer keys in kebab form for api docs transformer header --- src/Commands/MakeApiDocsCommand.php | 11 +---------- src/Http/Handlers.php | 2 +- stubs/Api/DetailHandler.stub | 15 +++++++++++++-- stubs/Api/PaginationHandler.stub | 15 +++++++++++++-- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index ed08046..18e13a3 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -22,7 +22,7 @@ public function handle(): int $serverNameSpace = 'App\\Virtual'; $serverFile = 'ApiDocsController.php'; - $virtualResourcePath = $baseServerPath . 'Filament/Resources'; + $virtualResourcePath = $baseServerPath . '/Filament/Resources'; $virtualResourceNameSpace = $serverNameSpace . '\\Filament\\Resources'; $isNotInstalled = $this->checkForCollision([$baseServerPath . '/' . $serverFile]); @@ -165,14 +165,6 @@ public function handle(): int } } - $resourceTransformers = []; - if (method_exists($resource, 'apiTransformers')) { - $transformers = ($modelNamespace . "\\" . $resourceClass)::apiTransformers(); - foreach($transformers as $transformer) { - $resourceTransformers[] = str($transformer)->afterLast('\\')->kebab(); - } - } - try { $handlerMethods = File::allFiles($handlerSourceDirectory); @@ -195,7 +187,6 @@ public function handle(): int 'handlerClass' => $handler, 'handlerName' => $handlerName, 'capitalsResource' => strtoupper($modelClass), - 'resourceTransformers' => $resourceTransformers, 'path' => '/' . str($pluralModelClass)->kebab(), ]); } diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index ae322a3..f11a0e6 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -105,7 +105,7 @@ public static function getApiTransformer(): ?string /** * @return array */ - protected static function getApiTransformers(): array + public static function getApiTransformers(): array { return array_merge([ 'default' => DefaultTransformer::class, diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index 38868c0..e920b0c 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -32,6 +32,17 @@ if ({{ handlerName }}Real::isPublic()) { 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; + } +} + +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')); @@ -70,11 +81,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); parameters: [ new OAT\HeaderParameter( - name: "{{ config('api-service.route.api_transformer_header') }}", + name: RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMER_HEADER_KEY, description: "Select a custom Transformer if needed", in: "header", allowEmptyValue: true, - schema: new OAT\Schema(enum: {{ resourceTransformers }}), + schema: new OAT\Schema(enum: RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMERS), ), // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 25096d4..e41d318 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -33,6 +33,17 @@ if ({{ handlerName }}Real::isPublic()) { 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; + } +} + +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')); @@ -72,11 +83,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); parameters: [ new OAT\HeaderParameter( - name: "{{ config('api-service.route.api_transformer_header') }}", + name: RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMER_HEADER_KEY, description: "Select a custom Transformer if needed", in: "header", allowEmptyValue: true, - schema: new OAT\Schema(enum: {{ resourceTransformers }}), + schema: new OAT\Schema(enum: RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMERS), ), // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, From 84eab670d5e52ea9c5a1d8dfc0f12106cac17afe Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 16:45:17 +0200 Subject: [PATCH 29/63] command generating api docs now also generates the defaultTransformer making it optional to use a own transformer of each resource in the api docs --- src/Commands/MakeApiDocsCommand.php | 19 +++++++++++++++++-- stubs/Api/CreateHandler.stub | 12 ++++++++++-- stubs/Api/DeleteHandler.stub | 8 +++++++- stubs/Api/DetailHandler.stub | 9 ++++++++- stubs/Api/PaginationHandler.stub | 9 ++++++++- stubs/Api/Transformer.stub | 2 +- stubs/Api/UpdateHandler.stub | 12 ++++++++++-- 7 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 18e13a3..1769f91 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -69,7 +69,7 @@ public function handle(): int $this->createDirectory('Virtual/Filament/Resources'); - $this->copyStubToApp('ApiDocsController', $baseServerPath . '/' . $serverFile, [ + $this->copyStubToApp('Api/ApiDocsController', $baseServerPath . '/' . $serverFile, [ 'namespace' => $serverNameSpace, 'title' => $serverTitle, 'version' => $serverVersion, @@ -79,6 +79,21 @@ public function handle(): int '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( @@ -156,7 +171,7 @@ public function handle(): int $stubVars = [ 'namespace' => $namespace, 'modelClass' => $pluralModelClass, - 'resourceClass' => $resourceClass, + 'resourceClass' => '\\' . $resourceClass . '\\Transformers', 'transformerName' => $transformerClass, ]; diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index 2277881..0d6e665 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -32,6 +32,10 @@ if ({{ handlerName }}Real::isPublic()) { 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", $has_custom_transformers ?? 0); + defined("PANEL_PREFIX_{{ capitalsResource }}_CREATE") ?: define("PANEL_PREFIX_{{ capitalsResource }}_CREATE", $panelPrefix); defined("BASE_URL") ?: define("BASE_URL", config('app.url')); @@ -72,12 +76,16 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); ], requestBody: new OAT\RequestBody( required: true, - content: new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") + content: (RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer/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"), - new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") + (RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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'), diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index 85aa2e3..5e83263 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -32,6 +32,10 @@ if ({{ handlerName }}Real::isPublic()) { 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", $has_custom_transformers ?? 0); + defined("PANEL_PREFIX_{{ capitalsResource }}_DELETE") ?: define("PANEL_PREFIX_{{ capitalsResource }}_DELETE", $panelPrefix); defined("BASE_URL") ?: define("BASE_URL", config('app.url')); @@ -81,7 +85,9 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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"), - new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items") + (RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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'), diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index e920b0c..fc0ec7a 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -38,8 +38,11 @@ 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", $has_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')); @@ -132,7 +135,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); ), ], responses: [ - new OAT\Response(response: 200, description: 'Operation succesful', content: new OAT\JsonContent(type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items")), + new OAT\Response(response: 200, description: 'Operation succesful', content: + (RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent(type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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'), diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index e41d318..92c8ef7 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -39,8 +39,11 @@ 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", $has_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')); @@ -127,7 +130,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); ), ], responses: [ - new OAT\Response(response: 200, description: 'Operation succesful', content: new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer")), + new OAT\Response(response: 200, description: 'Operation succesful', content: + (RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS > 0) ? + new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer") : + new OAT\JsonContent(ref: "#/components/schemas/DefaultTransformer") + ), new OAT\Response(response: 400, description: 'Bad Request'), new OAT\Response(response: 401, description: 'Unauthenticated'), new OAT\Response(response: 403, description: 'Forbidden'), diff --git a/stubs/Api/Transformer.stub b/stubs/Api/Transformer.stub index ffc7de6..6c4cefb 100644 --- a/stubs/Api/Transformer.stub +++ b/stubs/Api/Transformer.stub @@ -1,6 +1,6 @@ 0) ? + new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/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"), - new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") + (RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS > 0) ? + new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/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'), From 158308ab02cae86637fad4f976fbf37f0480cd5c Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 17:28:52 +0200 Subject: [PATCH 30/63] typo in variable name --- stubs/Api/CreateHandler.stub | 2 +- stubs/Api/DeleteHandler.stub | 2 +- stubs/Api/DetailHandler.stub | 2 +- stubs/Api/PaginationHandler.stub | 2 +- stubs/Api/UpdateHandler.stub | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index 0d6e665..db160c3 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -34,7 +34,7 @@ if ({{ handlerName }}Real::isPublic()) { $transformers = {{ handlerName }}Real::getApiTransformers(); $custom_transformers = count(array_except($transformers, ['default'])); -defined("RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS", $has_custom_transformers ?? 0); +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')); diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index 5e83263..7e8f914 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -34,7 +34,7 @@ if ({{ handlerName }}Real::isPublic()) { $transformers = {{ handlerName }}Real::getApiTransformers(); $custom_transformers = count(array_except($transformers, ['default'])); -defined("RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_DELETE_CUSTOM_TRANSFORMERS", $has_custom_transformers ?? 0); +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')); diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index fc0ec7a..6ff845f 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -41,7 +41,7 @@ if (!empty($transformers)) { $custom_transformers = count(array_except($transformers, ['default'])); } -defined("RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS", $has_custom_transformers ?? 0); +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')); diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 92c8ef7..8f49f16 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -42,7 +42,7 @@ if (!empty($transformers)) { $custom_transformers = count(array_except($transformers, ['default'])); } -defined("RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS", $has_custom_transformers ?? 0); +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')); diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index 9d00ff8..778db4c 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -34,7 +34,7 @@ if ({{ handlerName }}Real::isPublic()) { $transformers = {{ handlerName }}Real::getApiTransformers(); $custom_transformers = count(array_except($transformers, ['default'])); -defined("RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS") ?: define("RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS", $has_custom_transformers ?? 0); +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')); From 9fb02377d0becd4463b2c13ca08f2c9737c85f54 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 20:28:11 +0200 Subject: [PATCH 31/63] requests and responses of Api request can always be the custom or de default transformer schema. --- stubs/Api/CreateHandler.stub | 11 +++++++++-- stubs/Api/DeleteHandler.stub | 6 ++++-- stubs/Api/DetailHandler.stub | 5 ++++- stubs/Api/PaginationHandler.stub | 7 +++++-- stubs/Api/UpdateHandler.stub | 23 +++++++++++++++-------- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index db160c3..da0a621 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -77,14 +77,21 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); requestBody: new OAT\RequestBody( required: true, content: (RESOURCE_{{ capitalsResource }}_CREATE_CUSTOM_TRANSFORMERS > 0) ? - new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") : + 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", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") : + 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'), diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index 7e8f914..e1cb428 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -62,7 +62,6 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); description: 'ID of the tenant', default: "" ), - ] ) : new OAT\Server( @@ -86,7 +85,10 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items") : + 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'), diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index 6ff845f..5892626 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -137,7 +137,10 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); responses: [ new OAT\Response(response: 200, description: 'Operation succesful', content: (RESOURCE_{{ capitalsResource }}_DETAIL_CUSTOM_TRANSFORMERS > 0) ? - new OAT\JsonContent(type: "object", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items") : + 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'), diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 8f49f16..ce5a4b4 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -132,8 +132,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); responses: [ new OAT\Response(response: 200, description: 'Operation succesful', content: (RESOURCE_{{ capitalsResource }}_PAGINATION_CUSTOM_TRANSFORMERS > 0) ? - new OAT\JsonContent(ref: "#/components/schemas/{{ modelClass }}Transformer") : - new OAT\JsonContent(ref: "#/components/schemas/DefaultTransformer") + 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'), diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index 778db4c..f729f28 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -87,18 +87,25 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); content: new OAT\MediaType( mediaType: "application/json", schema: (RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS > 0) ? - new OAT\Schema(ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data/items") : + 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\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", ref: "#/components/schemas/{{ modelClass }}Transformer/properties/data") : - new OAT\Property(property: "data", type: "object", ref: "#/components/schemas/DefaultTransformer/properties/data") - ])), + 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'), From dc0f59d1105fd0c53bdf10c2b81765f88f87e798 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 20 Jun 2024 21:28:52 +0200 Subject: [PATCH 32/63] small issue in requestBody return schema --- stubs/Api/UpdateHandler.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index f729f28..b187170 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -87,7 +87,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); content: new OAT\MediaType( mediaType: "application/json", schema: (RESOURCE_{{ capitalsResource }}_UPDATE_CUSTOM_TRANSFORMERS > 0) ? - new OAT\JsonContent( + 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"), From 094a4a44e6713f2c5d861e95989ef06a37249281 Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 24 Jul 2024 15:11:11 +0200 Subject: [PATCH 33/63] auto generate DTO properies wehn available from laravel-data package. render multiple transformer api docs --- README.md | 66 +++++++++++++++++++++++++++++ src/Attributes/ApiPropertyInfo.php | 34 +++++++++++++++ src/Commands/MakeApiDocsCommand.php | 61 ++++++++++++++++++++------ src/Http/Handlers.php | 10 +++++ stubs/Api/Transformer.stub | 4 +- stubs/CreateHandler.stub | 10 ++++- stubs/CustomHandler.stub | 10 ++++- stubs/DetailHandler.stub | 8 +++- stubs/PaginationHandler.stub | 5 +++ stubs/UpdateHandler.stub | 9 +++- 10 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 src/Attributes/ApiPropertyInfo.php diff --git a/README.md b/README.md index 8673b41..e63b814 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,72 @@ $client->request('GET', '/api/blogs', [ This way the correct transformer will be used to give you the correct response json. +### 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. +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: '')] + 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: ''), + ] + ), + )], + public $data; + ... +``` + + ### Group Name & Prefix You can edit prefix & group route name as you want, default this plugin use model singular label; diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php new file mode 100644 index 0000000..b9e65e3 --- /dev/null +++ b/src/Attributes/ApiPropertyInfo.php @@ -0,0 +1,34 @@ +title = $title; + $this->description = $description; + $this->example = $example; + } + + public static function keyword(): string + { + return 'api-property-info'; + } + + public function parameters(): array + { + return [ + 'title', + 'description', + 'example' + ]; + } +} diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 1769f91..b7927d8 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -3,6 +3,7 @@ namespace Rupadana\ApiService\Commands; use Exception; +use ReflectionClass; use Filament\Support\Commands\Concerns\CanManipulateFiles; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; @@ -93,7 +94,6 @@ public function handle(): int if (!$this->checkForCollision(["{$baseServerPath}/{$transformerClass}.php"])) { $this->copyStubToApp("Api/Transformer", $baseServerPath . '/' . $transformerClass . '.php', $stubVarsDefaultTransformer); } - } $model = (string) str($this->argument('resource') ?? text( @@ -154,6 +154,9 @@ public function handle(): int default: $virtualResourceNameSpace ); + $modelOfResource = $resource::getModel(); + $modelDto = new $modelOfResource(); + $handlersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Handlers"; $transformersVirtualNamespace = "{$namespace}\\{$resourceClass}\\Transformers"; @@ -164,19 +167,29 @@ public function handle(): int if (method_exists($resource, 'getApiTransformer')) { // generate API transformer - $transformer = ($modelNamespace . "\\" . $resourceClass)::getApiTransformer(); - $transformerClassPath = (string) str($transformer); - $transformerClass = (string) str($transformerClassPath)->afterLast('\\'); - - $stubVars = [ - 'namespace' => $namespace, - 'modelClass' => $pluralModelClass, - 'resourceClass' => '\\' . $resourceClass . '\\Transformers', - 'transformerName' => $transformerClass, - ]; + $transformers = method_exists($resource, 'apiTransformers') ? ($modelNamespace . "\\" . $resourceClass)::apiTransformers() : [($modelNamespace . "\\" . $resourceClass)::getApiTransformer()]; + foreach ($transformers as $transformer) { + $transformerClassPath = (string) str($transformer); + $transformerClass = (string) str($transformerClassPath)->afterLast('\\'); + + $stubVars = [ + 'namespace' => $namespace, + 'modelClass' => $pluralModelClass, + 'resourceClass' => '\\' . $resourceClass . '\\Transformers', + 'transformerName' => $transformerClass, + ]; + if (property_exists($modelDto, 'dataClass') || method_exists($modelDto, 'dataClass')) { + $stubVars['transformerProperties'] = ''; + $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); + if (!$this->checkForCollision(["{$transformersVirtualDirectory}/{$transformerClass}.php"])) { + $this->copyStubToApp("Api/Transformer", $transformersVirtualDirectory . '/' . $transformerClass . '.php', $stubVars); + } } } @@ -216,6 +229,28 @@ public function handle(): int return static::SUCCESS; } + private function readModelDto(string $model): array + { + + $modelReflection = new ReflectionClass($model); + + $dtoClass = $modelReflection->getProperty('dataClass')->getDefaultValue(); + $dtoReflection = new ReflectionClass($dtoClass); + $properties = []; + + foreach ($dtoReflection->getProperties() as $property) { + if (!empty($property->getAttributes())) { + $attribute = $property->getAttributes()[0]; + array_push( + $properties, + "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType() . "', title: '" . $property->getName() . "', description: '" . ($attribute->getArguments()["description"] ?? '') . "', example: '" . ($attribute->getArguments()["example"] ?? '') . "')" + ); + } + } + + return $properties; + } + private function createDirectory(string $path): void { $path = app_path($path); diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index f11a0e6..bc4eb0e 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -2,6 +2,7 @@ namespace Rupadana\ApiService\Http; +use ReflectionClass; use Filament\Facades\Filament; use Filament\Panel; use Illuminate\Database\Eloquent\Model; @@ -97,6 +98,15 @@ public static function getModel() return static::$resource::getModel(); } + public static function getDto(): ?string + { + $modelReflection = new ReflectionClass(static::getModel()); + if (property_exists(static::getModel(), 'dataClass')) { + return $modelReflection->getProperty('dataClass')->getDefaultValue(); + } + return null; + } + public static function getApiTransformer(): ?string { return static::getTransformerFromRequestHeader(); diff --git a/stubs/Api/Transformer.stub b/stubs/Api/Transformer.stub index 6c4cefb..f7d4700 100644 --- a/stubs/Api/Transformer.stub +++ b/stubs/Api/Transformer.stub @@ -19,8 +19,8 @@ class {{ transformerName }} { type: "array", items: new OAT\Items( properties: [ - new OAT\Property(property: "id", type: "integer", title: "ID", description: "id of the product", example: ""), - // add your own properties corresponding to the {{ transformerName }} + {{ transformerProperties }} + // add/modify your own properties corresponding to the {{ transformerName }} ] ), )] 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 cff8701..cf2968d 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 +} From 46a747712cf08cfcc39103fbb4f36861c116086b Mon Sep 17 00:00:00 2001 From: Eelco Date: Wed, 24 Jul 2024 17:12:38 +0200 Subject: [PATCH 34/63] small fix in stubVars when no DTO is found on the Resource --- src/Commands/MakeApiDocsCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index b7927d8..fe996a2 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -177,9 +177,10 @@ public function handle(): int 'modelClass' => $pluralModelClass, 'resourceClass' => '\\' . $resourceClass . '\\Transformers', 'transformerName' => $transformerClass, + 'transformerProperties' => '', ]; + if (property_exists($modelDto, 'dataClass') || method_exists($modelDto, 'dataClass')) { - $stubVars['transformerProperties'] = ''; $dtoProperties = $this->readModelDto($modelDto::class); foreach ($dtoProperties as $dtoLine) { $stubVars['transformerProperties'] .= $dtoLine . ","; From 82ccf51053aecc402ed62975ed70ab099f1dc722 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 20:58:02 +0200 Subject: [PATCH 35/63] small change in code --- src/Commands/MakeApiDocsCommand.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index fe996a2..a92dbc9 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -237,15 +237,11 @@ private function readModelDto(string $model): array $dtoClass = $modelReflection->getProperty('dataClass')->getDefaultValue(); $dtoReflection = new ReflectionClass($dtoClass); - $properties = []; foreach ($dtoReflection->getProperties() as $property) { if (!empty($property->getAttributes())) { $attribute = $property->getAttributes()[0]; - array_push( - $properties, - "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType() . "', title: '" . $property->getName() . "', description: '" . ($attribute->getArguments()["description"] ?? '') . "', example: '" . ($attribute->getArguments()["example"] ?? '') . "')" - ); + $properties[] = "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType() . "', title: '" . $property->getName() . "', description: '" . ($attribute->getArguments()["description"] ?? '') . "', example: '" . ($attribute->getArguments()["example"] ?? '') . "')"; } } From c697d40b6004d668507011e2ee7748442dd4229a Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 21:33:25 +0200 Subject: [PATCH 36/63] allow dynamic properties added to the #[ApiPropertyInfo( ....) Attribute --- src/Commands/MakeApiDocsCommand.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index a92dbc9..8753a98 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -241,7 +241,17 @@ private function readModelDto(string $model): array foreach ($dtoReflection->getProperties() as $property) { if (!empty($property->getAttributes())) { $attribute = $property->getAttributes()[0]; - $properties[] = "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType() . "', title: '" . $property->getName() . "', description: '" . ($attribute->getArguments()["description"] ?? '') . "', example: '" . ($attribute->getArguments()["example"] ?? '') . "')"; + + + $propertyTxt = ""; + $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', title: '" . $property->getName() . "', "; + + foreach ($attribute->getArguments() as $key => $argument) { + $propertyTxt .= $key . ": '" . $argument . "', "; + } + $propertyTxt = rtrim($propertyTxt, ', '); + $propertyTxt .= ")"; + $properties[] = $propertyTxt; } } From 287098bb2478df7d0acf3bc0280bdf9cc72d4566 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 21:45:41 +0200 Subject: [PATCH 37/63] ApiPropertyInfo now has AllowDynamicProperties for handling dynamic Attribute getters and setters --- src/Attributes/ApiPropertyInfo.php | 1 + src/Commands/MakeApiDocsCommand.php | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php index b9e65e3..7988890 100644 --- a/src/Attributes/ApiPropertyInfo.php +++ b/src/Attributes/ApiPropertyInfo.php @@ -4,6 +4,7 @@ use Attribute; +#[\AllowDynamicProperties] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] class ApiPropertyInfo { diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 8753a98..74921b0 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -242,7 +242,6 @@ private function readModelDto(string $model): array if (!empty($property->getAttributes())) { $attribute = $property->getAttributes()[0]; - $propertyTxt = ""; $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', title: '" . $property->getName() . "', "; From 5ce8931bf4d04e414a12add423907f87aa114e90 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 22:10:18 +0200 Subject: [PATCH 38/63] adding examples in the readme for extra optional Attribute fields --- README.md | 16 ++++++++++++++-- src/Attributes/ApiPropertyInfo.php | 14 +++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e63b814..a196b45 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ It is possible to use Spatie/Laravel-data package to autogenerate the correct da 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. See the last item in the example. So this is how you can implement those: ```php @@ -258,7 +259,8 @@ class BlogData extends Data public ?int $id, #[ApiPropertyInfo(description: 'Name of the Blog', example: '')] public string $name, - #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '')] + #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', ["ref" => "MyBlogSchema", "", "oneOf" => '{ new OAT\Schema(type="string"), new OAT\Schema(type="integer")}'] + )] public string $image, ) { } @@ -292,7 +294,17 @@ class BlogTransformer { 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: ''), + 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") + }), ] ), )], diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php index 7988890..0d1b0f8 100644 --- a/src/Attributes/ApiPropertyInfo.php +++ b/src/Attributes/ApiPropertyInfo.php @@ -11,12 +11,24 @@ class ApiPropertyInfo public string $title; public string $description; public string $example; + private array $extraProperties; - public function __construct(string $title = '', string $description = '', string $example = '') + public function __construct(string $title = '', string $description = '', string $example = '', $extraProperties = []) { $this->title = $title; $this->description = $description; $this->example = $example; + $this->extraProperties = $extraProperties; + } + + public function __get($name) + { + return $this->extraProperties[$name] ?? null; + } + + public function __isset($name) + { + return isset($this->extraProperties[$name]); } public static function keyword(): string From 431335c46e55d21178122f32358fa57190fcfbc6 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 22:12:27 +0200 Subject: [PATCH 39/63] move the laravel-data readme part to the bottom --- README.md | 155 +++++++++++++++++++++++++++--------------------------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index a196b45..b54b111 100644 --- a/README.md +++ b/README.md @@ -235,84 +235,6 @@ $client->request('GET', '/api/blogs', [ This way the correct transformer will be used to give you the correct response json. -### 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. 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" => "MyBlogSchema", "", "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; - ... -``` - - ### Group Name & Prefix You can edit prefix & group route name as you want, default this plugin use model singular label; @@ -467,6 +389,83 @@ class BlogTransformer { You can find more about all possible properties at https://zircote.github.io/swagger-php/reference/attributes.html#property +### 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. 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" => "MyBlogSchema", "", "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). From 06f287376711f8e0c1c1a81037f9cb7cffd6f66f Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 22:12:55 +0200 Subject: [PATCH 40/63] remove some empty line endings --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b54b111..bcc138e 100644 --- a/README.md +++ b/README.md @@ -449,11 +449,11 @@ class BlogTransformer { 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: '', + property: 'image', + type: 'string', + title: 'image', + description: 'Image Url of the Blog', + example: '', ref: "MyBlogSchema", oneOf: { new OAT\Schema(type="string"), From d25b2c676c1757aa734c0884ba351517ed012770 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 22:14:04 +0200 Subject: [PATCH 41/63] small readme styling optimizations --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bcc138e..15e413a 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ class BlogResource extends Resource */ public static function apiTransformers(): array { - return + return [ BlogTransformer::class, ModifiedBlogTransformer::class, @@ -322,7 +322,7 @@ class PaginationHandler extends Handlers { 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 ``` @@ -387,7 +387,7 @@ class BlogTransformer { ``` -You can find more about all possible properties at https://zircote.github.io/swagger-php/reference/attributes.html#property +You can find more about all possible properties at ### Laravel-Data package integration (optional) From 8a4276ae86c7668c5d727092df1f9a95115868a3 Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 22:17:17 +0200 Subject: [PATCH 42/63] typehint param in constructor --- src/Attributes/ApiPropertyInfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php index 0d1b0f8..2337c79 100644 --- a/src/Attributes/ApiPropertyInfo.php +++ b/src/Attributes/ApiPropertyInfo.php @@ -13,7 +13,7 @@ class ApiPropertyInfo public string $example; private array $extraProperties; - public function __construct(string $title = '', string $description = '', string $example = '', $extraProperties = []) + public function __construct(string $title = '', string $description = '', string $example = '', array $extraProperties = []) { $this->title = $title; $this->description = $description; From 9a45e797677f417a6c39b10abc38f1966d21367a Mon Sep 17 00:00:00 2001 From: Eelco Date: Thu, 25 Jul 2024 23:02:00 +0200 Subject: [PATCH 43/63] correct formatting of the extraProperties --- README.md | 8 ++++---- src/Commands/MakeApiDocsCommand.php | 28 ++++++++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 15e413a..05531fb 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,7 @@ It is possible to use Spatie/Laravel-data package to autogenerate the correct da 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. See the last item in the example. +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 @@ -413,7 +413,7 @@ class BlogData extends Data public ?int $id, #[ApiPropertyInfo(description: 'Name of the Blog', example: '')] public string $name, - #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', ["ref" => "MyBlogSchema", "", "oneOf" => '{ new OAT\Schema(type="string"), new OAT\Schema(type="integer")}'] + #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', extraProperties: ["ref" => "'ImageBlogSchema'", "oneOf" => '[ new OAT\Schema(type:"string"), new OAT\Schema(type:"integer")]'] )] public string $image, ) { @@ -455,10 +455,10 @@ class BlogTransformer { description: 'Image Url of the Blog', example: '', ref: "MyBlogSchema", - oneOf: { + oneOf: [ new OAT\Schema(type="string"), new OAT\Schema(type="integer") - }), + ]), ] ), )], diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 74921b0..ed6853a 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -7,6 +7,7 @@ use Filament\Support\Commands\Concerns\CanManipulateFiles; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; +use Rupadana\ApiService\Attributes\ApiPropertyInfo; use function Laravel\Prompts\text; @@ -232,7 +233,6 @@ public function handle(): int private function readModelDto(string $model): array { - $modelReflection = new ReflectionClass($model); $dtoClass = $modelReflection->getProperty('dataClass')->getDefaultValue(); @@ -241,16 +241,24 @@ private function readModelDto(string $model): array foreach ($dtoReflection->getProperties() as $property) { if (!empty($property->getAttributes())) { $attribute = $property->getAttributes()[0]; - - $propertyTxt = ""; - $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', title: '" . $property->getName() . "', "; - - foreach ($attribute->getArguments() as $key => $argument) { - $propertyTxt .= $key . ": '" . $argument . "', "; + if (strpos($attribute->getName(), ApiPropertyInfo::class) !== false) { + $propertyTxt = ""; + $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', title: '" . $property->getName() . "', "; + + + foreach ($attribute->getArguments() as $key => $argument) { + if ($key == 'extraProperties') { + foreach ($argument as $extraKey => $extraArgument) { + $propertyTxt .= $extraKey . ": " . $extraArgument . ", "; + } + } else { + $propertyTxt .= $key . ": '" . $argument . "', "; + } + } + $propertyTxt = rtrim($propertyTxt, ', '); + $propertyTxt .= ")"; + $properties[] = $propertyTxt; } - $propertyTxt = rtrim($propertyTxt, ', '); - $propertyTxt .= ")"; - $properties[] = $propertyTxt; } } From fe962f493e1fe759948a0ba56d6b839e89b328ab Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 26 Jul 2024 00:31:28 +0200 Subject: [PATCH 44/63] read property types from OpenApi\Attributes $_types array. and use those to determine how to parse the data in the Property() field. We can now use dynamic properties again because extending the OpenApi\Property class --- README.md | 2 +- src/Attributes/ApiPropertyInfo.php | 35 ++++++----------------------- src/Commands/MakeApiDocsCommand.php | 17 +++++++++----- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 05531fb..9fbd89b 100644 --- a/README.md +++ b/README.md @@ -413,7 +413,7 @@ class BlogData extends Data public ?int $id, #[ApiPropertyInfo(description: 'Name of the Blog', example: '')] public string $name, - #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', extraProperties: ["ref" => "'ImageBlogSchema'", "oneOf" => '[ new OAT\Schema(type:"string"), new OAT\Schema(type:"integer")]'] + #[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', ref: "ImageBlogSchema", oneOf: '[new OAT\Schema(type:"string"), new OAT\Schema(type:"integer")]'] )] public string $image, ) { diff --git a/src/Attributes/ApiPropertyInfo.php b/src/Attributes/ApiPropertyInfo.php index 2337c79..8f2c056 100644 --- a/src/Attributes/ApiPropertyInfo.php +++ b/src/Attributes/ApiPropertyInfo.php @@ -3,34 +3,13 @@ namespace Rupadana\ApiService\Attributes; use Attribute; +use OpenApi\Attributes\Property; + #[\AllowDynamicProperties] #[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] -class ApiPropertyInfo +class ApiPropertyInfo extends Property { - public string $title; - public string $description; - public string $example; - private array $extraProperties; - - public function __construct(string $title = '', string $description = '', string $example = '', array $extraProperties = []) - { - $this->title = $title; - $this->description = $description; - $this->example = $example; - $this->extraProperties = $extraProperties; - } - - public function __get($name) - { - return $this->extraProperties[$name] ?? null; - } - - public function __isset($name) - { - return isset($this->extraProperties[$name]); - } - public static function keyword(): string { return 'api-property-info'; @@ -39,9 +18,9 @@ public static function keyword(): string public function parameters(): array { return [ - 'title', - 'description', - 'example' - ]; + 'title', + 'description', + 'example' + ]; } } diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index ed6853a..f1ab2bf 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -7,6 +7,7 @@ use Filament\Support\Commands\Concerns\CanManipulateFiles; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; +use OpenApi\Attributes as OAT; use Rupadana\ApiService\Attributes\ApiPropertyInfo; use function Laravel\Prompts\text; @@ -233,6 +234,13 @@ public function handle(): int 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(); @@ -241,16 +249,15 @@ private function readModelDto(string $model): array 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() . "', title: '" . $property->getName() . "', "; - foreach ($attribute->getArguments() as $key => $argument) { - if ($key == 'extraProperties') { - foreach ($argument as $extraKey => $extraArgument) { - $propertyTxt .= $extraKey . ": " . $extraArgument . ", "; - } + + if (array_key_exists($key, $openApiAttrTypes) && $openApiAttrTypes[$key] !== 'string') { + $propertyTxt .= $key . ": " . $argument . ", "; } else { $propertyTxt .= $key . ": '" . $argument . "', "; } From 279c70d29266d71f9ffe0899ec1eb31d41f73bd0 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 26 Jul 2024 00:36:41 +0200 Subject: [PATCH 45/63] remove title: as a base / static attribute and make it dynamic via the ApiPropertyInfo --- src/Commands/MakeApiDocsCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index f1ab2bf..0dc121b 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -252,7 +252,7 @@ private function readModelDto(string $model): array if (strpos($attribute->getName(), ApiPropertyInfo::class) !== false) { $propertyTxt = ""; - $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', title: '" . $property->getName() . "', "; + $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', "; foreach ($attribute->getArguments() as $key => $argument) { From 446900968b6811707c3bf5645078d59ddf0fb3e9 Mon Sep 17 00:00:00 2001 From: Eelco Date: Fri, 26 Jul 2024 11:20:50 +0200 Subject: [PATCH 46/63] make sure array and object types get the extra OAT\Items() and schema: '{}' --- src/Commands/MakeApiDocsCommand.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index 0dc121b..b82f190 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -252,12 +252,24 @@ private function readModelDto(string $model): array if (strpos($attribute->getName(), ApiPropertyInfo::class) !== false) { $propertyTxt = ""; + $propertyTxt .= "new OAT\Property(property: '" . $property->getName() . "', type: '" . $property->getType()->getName() . "', "; - foreach ($attribute->getArguments() as $key => $argument) { + $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 .= $key . ": '" . $argument . "', "; + + $propertyTxt .= match ($argument) { + 'array' => "items: new OAT\Items(), ", + 'object' => "schema: '{}', ", + default => '', + }; } else { $propertyTxt .= $key . ": '" . $argument . "', "; } From 361c5c4811f6f5467d6a0be77f8a24ef8fea0051 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 14:48:11 +0200 Subject: [PATCH 47/63] initial setup for version support via URL path/query/header --- config/api-service.php | 3 ++ src/ApiService.php | 63 +++++++++++++++++++++++++++++++++++------- src/Http/Handlers.php | 56 +++++++++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/config/api-service.php b/config/api-service.php index 1d473ad..eb19119 100644 --- a/config/api-service.php +++ b/config/api-service.php @@ -19,6 +19,9 @@ 'route' => [ 'panel_prefix' => true, 'use_resource_middlewares' => false, + 'default_transformer_name' => 'default', + 'api_version_method' => 'path', // 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' => [ diff --git a/src/ApiService.php b/src/ApiService.php index 0e65028..4662fa5 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 @@ -36,17 +38,48 @@ public static function getResource() public static function registerRoutes(Panel $panel) { - $slug = static::getResource()::getSlug(); + $versionPrefix = ''; + + if (static::getApiVersionMethod() === 'path') { + + $transformers = static::getResource()::getApiTransformers(); + + foreach ($transformers as $transKey => $transformer) { + + $versionPrefix = '{' . self::getApiVersionParameterName() . '}/'; + $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('.'); - $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 +87,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 +96,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'); + } + + public static function getApiVersionParameterName(): string + { + return config('api-service.route.api_version_parameter_name'); + } } diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index bc4eb0e..5cf8635 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -109,7 +109,11 @@ public static function getDto(): ?string public static function getApiTransformer(): ?string { - return static::getTransformerFromRequestHeader(); + return match (ApiService::getApiVersionMethod()) { + 'path' => static::getTransformerFromUrlPath(), + 'query' => static::getTransformerFromUrlQuery(), + 'header' => static::getTransformerFromRequestHeader(), + }; } /** @@ -118,7 +122,7 @@ public static function getApiTransformer(): ?string public static function getApiTransformers(): array { return array_merge([ - 'default' => DefaultTransformer::class, + ApiService::getDefaultTransformerName() => DefaultTransformer::class, ], method_exists(static::$resource, 'apiTransformers') ? array_combine( array_map(fn ($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), $transformers @@ -126,15 +130,55 @@ public static function getApiTransformers(): array } /** - * @throws Exception + * @throws TransformerNotFoundException + */ + protected static function getTransformerFromUrlPath(): string + { + // $routeApiVersion = request()->segment(1); + $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 self::getApiTransformers()[$transformer]; + + } + + /** + * @throws TransformerNotFoundException */ protected static function getTransformerFromRequestHeader(): string { - $headerName = config('api-service.route.api_transformer_header'); - $headerName = strtolower($headerName); + $headerName = strtolower(config('api-service.route.api_transformer_header')); if (!request()->headers->has($headerName)) { if (!method_exists(static::$resource, 'getApiTransformer')) { - return self::getApiTransformers()['default']; + return self::getApiTransformers()[ApiService::getDefaultTransformerName()]; } return static::$resource::getApiTransformer(); } From 2eb2a8150645540467eef2d9cb7d30b54e422d3c Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 15:55:07 +0200 Subject: [PATCH 48/63] getApiTransformers() now with key -> values so needed to change this at some places to reflect that. --- README.md | 83 +++++++++++++++++++---------- config/api-service.php | 2 +- src/Commands/MakeApiDocsCommand.php | 2 +- src/Http/Handlers.php | 14 ++--- 4 files changed, 64 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 1ace347..e8d72a2 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,11 @@ So, You don't need to register the routes manually. The routes will be : -| Method | Endpoint | Description | -| ------ | -------------------- | --------------------------- | -| GET | /api/`admin`/blogs | Return LengthAwarePaginator | -| GET | /api/`admin`/blogs/1 | Return single resource | -| PUT | /api/`admin`/blogs/1 | Update resource | -| POST | /api/`admin`/blogs | Create resource | -| DELETE | /api/`admin`/blogs/1 | Delete resource | +- [GET] '/api/`admin`/blogs' - Return LengthAwarePaginator +- [GET] '/api/`admin`/blogs/1' - Return single resource +- [PUT] '/api/`admin`/blogs/1' - Update resource +- [POST] '/api/`admin`/blogs' - Create resource +- [DELETE] '/api/`admin`/blogs/1' - Delete resource On CreateHandler, you need to be create your custom request validation. @@ -92,34 +90,28 @@ Token Resource is protected by TokenPolicy. You can disable it by publishing the ], ``` -> [!IMPORTANT] -> If you use Laravel 11, don't forget to run ``` php artisan install:api ``` to publish the personal_access_tokens migration after that run ``` php artisan migrate ``` to migrate the migration, but as default if you run the ``` php artisan install:api ``` it will ask you to migrate your migration. - ### 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. +You can specified `allowedFilters` and `allowedFields` in your model. For example: ```php -class User extends Model implements HasAllowedFields, HasAllowedSorts, HasAllowedFilters { +class User extends Model { // Which fields can be selected from the database through the query string - public function getAllowedFields(): array - { - // Your implementation here - } + public static array $allowedFields = [ + 'name' + ]; // Which fields can be used to sort the results through the query string - public function getAllowedSorts(): array - { - // Your implementation here - } + public static array $allowedSorts = [ + 'name', + 'created_at' + ]; // Which fields can be used to filter the results through the query string - public function getAllowedFilters(): array - { - // Your implementation here - } + public static array $allowedFilters = [ + 'name' + ]; } ``` @@ -210,9 +202,9 @@ class BlogResource extends Resource { return [ - BlogTransformer::class, - ModifiedBlogTransformer::class, - ExtraBlogColumnsTransformer::class, + 'blog' => BlogTransformer::class, + 'mod-blog' => ModifiedBlogTransformer::class, + 'extra-blog-column' => ExtraBlogColumnsTransformer::class, ... etc. ] } @@ -243,6 +235,41 @@ $client->request('GET', '/api/blogs', [ 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' the 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 in the URL you can add an extra parameter 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: +'' + +Note: 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; diff --git a/config/api-service.php b/config/api-service.php index eb19119..34c96a4 100644 --- a/config/api-service.php +++ b/config/api-service.php @@ -20,7 +20,7 @@ 'panel_prefix' => true, 'use_resource_middlewares' => false, 'default_transformer_name' => 'default', - 'api_version_method' => 'path', // options: ['path', 'query', 'headers'] + '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'), ], diff --git a/src/Commands/MakeApiDocsCommand.php b/src/Commands/MakeApiDocsCommand.php index b82f190..b004b0c 100644 --- a/src/Commands/MakeApiDocsCommand.php +++ b/src/Commands/MakeApiDocsCommand.php @@ -170,7 +170,7 @@ public function handle(): int if (method_exists($resource, 'getApiTransformer')) { // generate API transformer $transformers = method_exists($resource, 'apiTransformers') ? ($modelNamespace . "\\" . $resourceClass)::apiTransformers() : [($modelNamespace . "\\" . $resourceClass)::getApiTransformer()]; - foreach ($transformers as $transformer) { + foreach ($transformers as $transKey => $transformer) { $transformerClassPath = (string) str($transformer); $transformerClass = (string) str($transformerClassPath)->afterLast('\\'); diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 5cf8635..28476bf 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -123,10 +123,11 @@ public static function getApiTransformers(): array { return array_merge([ ApiService::getDefaultTransformerName() => DefaultTransformer::class, - ], method_exists(static::$resource, 'apiTransformers') ? array_combine( + ], method_exists(static::$resource, 'apiTransformers') ? + array_flip(array_combine( array_map(fn ($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), - $transformers - ) : []); // @phpstan-ignore-line + array_keys($transformers) + )) : []); // @phpstan-ignore-line } /** @@ -134,11 +135,10 @@ public static function getApiTransformers(): array */ protected static function getTransformerFromUrlPath(): string { - // $routeApiVersion = request()->segment(1); $routeApiVersion = request()->route(ApiService::getApiVersionParameterName()); $transformer = Str::kebab($routeApiVersion); - if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { throw new TransformerNotFoundException($transformer); } @@ -162,7 +162,7 @@ protected static function getTransformerFromUrlQuery(): string $transformer = request()->input($queryName); $transformer = Str::kebab($transformer); - if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { throw new TransformerNotFoundException($transformer); } @@ -186,7 +186,7 @@ protected static function getTransformerFromRequestHeader(): string $transformer = request()->headers->get($headerName); $transformer = Str::kebab($transformer); - if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { + if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { throw new TransformerNotFoundException($transformer); } From 56dbdbf5cac27d1d68e4f9d9eb5d39f47f899525 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 16:11:54 +0200 Subject: [PATCH 49/63] typo and text changed in readme.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e8d72a2..4fe6cee 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Alternative methods could also be used, like using as a prefix in the URL path o 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' the 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. +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: '' @@ -264,11 +264,11 @@ public static function apiTransformers(): array } ``` -Another method is using `route.api_version_method` to 'query'. This way in the URL you can add an extra parameter with the name which you defined in your config under `route.api_version_parameter_name` +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: '' -Note: You can only use one method for this package for API versioning. +IMPORTANT: You can only use one method for this package for API versioning. ### Group Name & Prefix From dc316f91e2220d300a096c4d0accd96ebbe895c8 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 21:28:19 +0200 Subject: [PATCH 50/63] Cherry pick Readme.md from main branch --- README.md | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4fe6cee..3514b05 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,13 @@ So, You don't need to register the routes manually. The routes will be : -- [GET] '/api/`admin`/blogs' - Return LengthAwarePaginator -- [GET] '/api/`admin`/blogs/1' - Return single resource -- [PUT] '/api/`admin`/blogs/1' - Update resource -- [POST] '/api/`admin`/blogs' - Create resource -- [DELETE] '/api/`admin`/blogs/1' - Delete resource +| Method | Endpoint | Description | +| ------ | -------------------- | --------------------------- | +| GET | /api/`admin`/blogs | Return LengthAwarePaginator | +| GET | /api/`admin`/blogs/1 | Return single resource | +| PUT | /api/`admin`/blogs/1 | Update resource | +| POST | /api/`admin`/blogs | Create resource | +| DELETE | /api/`admin`/blogs/1 | Delete resource | On CreateHandler, you need to be create your custom request validation. @@ -90,28 +92,33 @@ Token Resource is protected by TokenPolicy. You can disable it by publishing the ], ``` +> [!IMPORTANT] +> If you use Laravel 11, don't forget to run ``` php artisan install:api ``` to publish the personal_access_tokens migration after that run ``` php artisan migrate ``` to migrate the migration, but as default if you run the ``` php artisan install:api ``` it will ask you to migrate your migration. + ### 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. -You can specified `allowedFilters` and `allowedFields` in your model. For example: +In order to allow modifying the query for your model you can implement the `HasAllowedFields`, `HasAllowedSorts` and `HasAllowedFilters` Contracts in your model. ```php -class User extends Model { +class User extends Model implements HasAllowedFields, HasAllowedSorts, HasAllowedFilters { // Which fields can be selected from the database through the query string - public static array $allowedFields = [ - 'name' - ]; + public function getAllowedFields(): array + { + // Your implementation here + } // Which fields can be used to sort the results through the query string - public static array $allowedSorts = [ - 'name', - 'created_at' - ]; + public function getAllowedSorts(): array + { + // Your implementation here + } // Which fields can be used to filter the results through the query string - public static array $allowedFilters = [ - 'name' - ]; + public function getAllowedFilters(): array + { + // Your implementation here + } } ``` From 948824145aad6c838fd68b1587e164a5a22e49e8 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 21:34:20 +0200 Subject: [PATCH 51/63] add apiTransformers() to resource stub. update readme about this function --- README.md | 2 +- stubs/ResourceApiService.stub | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3514b05..1322f3d 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ use App\Filament\Resources\BlogResource\Api\Transformers\ExtraBlogColumnsTransfo class BlogResource extends Resource { /** - * @return array + * @return array */ public static function apiTransformers(): array { diff --git a/stubs/ResourceApiService.stub b/stubs/ResourceApiService.stub index 064d5bc..1af5c87 100644 --- a/stubs/ResourceApiService.stub +++ b/stubs/ResourceApiService.stub @@ -10,6 +10,14 @@ class {{ apiServiceClass }} extends ApiService { protected static string | null $resource = {{ resourceClass }}::class; + /** + * @return array + */ + public static function apiTransformers(): array + { + return []; + } + public static function handlers() : array { return [ From 292b1cc6854504a571db276c55bd42553a661a52 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 21:35:45 +0200 Subject: [PATCH 52/63] Revert "add apiTransformers() to resource stub. update readme about this function" This reverts commit 948824145aad6c838fd68b1587e164a5a22e49e8. --- README.md | 2 +- stubs/ResourceApiService.stub | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 1322f3d..3514b05 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ use App\Filament\Resources\BlogResource\Api\Transformers\ExtraBlogColumnsTransfo class BlogResource extends Resource { /** - * @return array + * @return array */ public static function apiTransformers(): array { diff --git a/stubs/ResourceApiService.stub b/stubs/ResourceApiService.stub index 1af5c87..064d5bc 100644 --- a/stubs/ResourceApiService.stub +++ b/stubs/ResourceApiService.stub @@ -10,14 +10,6 @@ class {{ apiServiceClass }} extends ApiService { protected static string | null $resource = {{ resourceClass }}::class; - /** - * @return array - */ - public static function apiTransformers(): array - { - return []; - } - public static function handlers() : array { return [ From 8a7392c6d2fb4f46bb7320be6fd9d4ace99c0078 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 21:36:58 +0200 Subject: [PATCH 53/63] readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3514b05..1322f3d 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ use App\Filament\Resources\BlogResource\Api\Transformers\ExtraBlogColumnsTransfo class BlogResource extends Resource { /** - * @return array + * @return array */ public static function apiTransformers(): array { From a2d57ed5150df0a2603196165ec2120399047486 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 21:38:40 +0200 Subject: [PATCH 54/63] reflect published config in readme.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1322f3d..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, From fea4cf3926459a1aae326c8008c80e440edfe8d4 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 22:46:37 +0200 Subject: [PATCH 55/63] added basic api docs variables for the three methods of API versioning. --- stubs/Api/CreateHandler.stub | 68 +++++++++++++++++++++++++++++-- stubs/Api/DeleteHandler.stub | 66 ++++++++++++++++++++++++++++-- stubs/Api/DetailHandler.stub | 69 ++++++++++++++++++++++++++++++-- stubs/Api/PaginationHandler.stub | 68 +++++++++++++++++++++++++++++-- stubs/Api/UpdateHandler.stub | 69 ++++++++++++++++++++++++++++++-- 5 files changed, 322 insertions(+), 18 deletions(-) diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index da0a621..651a711 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -16,6 +16,36 @@ $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}/'); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); +} + +function apiVersionHeaders() { + 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() && @@ -49,7 +79,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Server( description: "API Server Filament Panel", - url: BASE_URL . "/api/{panel}" . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/{tenant}", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/{tenant}", variables: [ new OAT\ServerVariable( serverVariable: "panel", @@ -63,16 +93,48 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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/" . PANEL_PREFIX_{{ capitalsResource }}_CREATE . "/", + 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\Parameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: 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, diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index e1cb428..ce500c5 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -16,6 +16,35 @@ $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}/'); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); +} + +function apiVersionHeaders() { + 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() && @@ -49,7 +78,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Server( description: "API Server Filament Panel", - url: BASE_URL . "/api/{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/{tenant}", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/{tenant}", variables: [ new OAT\ServerVariable( serverVariable: "panel", @@ -62,11 +91,27 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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/" . PANEL_PREFIX_{{ capitalsResource }}_DELETE . "/", + 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, @@ -80,6 +125,21 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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\Parameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: 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: [ diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index 5892626..f9f1673 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -16,6 +16,36 @@ $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}/'); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); +} + +function apiVersionHeaders() { + 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() && @@ -59,7 +89,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Server( description: "API Server Filament Panel", - url: BASE_URL . "/api/{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/{tenant}", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/{tenant}", variables: [ new OAT\ServerVariable( serverVariable: "panel", @@ -72,12 +102,27 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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/" . PANEL_PREFIX_{{ capitalsResource }}_DETAIL . "/", + 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, @@ -93,6 +138,22 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); // (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\Parameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: 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", diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index ce5a4b4..3b21d95 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -17,6 +17,35 @@ $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}/'); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); +} + +function apiVersionHeaders() { + 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() && @@ -59,7 +88,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Server( description: "API Server Filament Panel", - url: BASE_URL . "/api/{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", variables: [ new OAT\ServerVariable( serverVariable: "panel", @@ -72,12 +101,27 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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/" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + 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 }}", @@ -96,6 +140,22 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); // (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\Parameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: 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", diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index b187170..161239d 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -16,6 +16,35 @@ $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}/'); +} + +function apiVersionQuery() { + defined("API_VERSION_URL_QUERY") ?: define("API_VERSION_URL_QUERY", true); +} + +function apiVersionHeaders() { + 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() && @@ -49,7 +78,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Server( description: "API Server Filament Panel", - url: BASE_URL . "/api/{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", + url: BASE_URL . "/api/" . API_VERSION_URL_PATH . "{panel}" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/{tenant}", variables: [ new OAT\ServerVariable( serverVariable: "panel", @@ -62,17 +91,49 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); 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/" . PANEL_PREFIX_{{ capitalsResource }}_PAGINATION . "/", + 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\Parameter( + name: API_TRANSFORMER_HEADER, + in: "header", + required: 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", From 25ed406872e9a67b6616cac3cdcaeb6b154666a7 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 22:51:42 +0200 Subject: [PATCH 56/63] when version via HTTP Header then its a OAT\HeaderParameter() --- stubs/Api/CreateHandler.stub | 3 ++- stubs/Api/DeleteHandler.stub | 3 ++- stubs/Api/DetailHandler.stub | 11 ++--------- stubs/Api/PaginationHandler.stub | 11 ++--------- stubs/Api/UpdateHandler.stub | 3 ++- 5 files changed, 10 insertions(+), 21 deletions(-) diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index 651a711..7e94c97 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -128,10 +128,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); schema: new OAT\Schema(type: "string") ) : null, - (API_VERSION_URL_HEADERS) ? new OAT\Parameter( + (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, diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index ce500c5..5a45458 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -133,10 +133,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); schema: new OAT\Schema(type: "string") ) : null, - (API_VERSION_URL_HEADERS) ? new OAT\Parameter( + (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, diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index f9f1673..c4322a1 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -128,14 +128,6 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_DETAIL) ? [["bearerAuth" => []]] : null, parameters: [ - new OAT\HeaderParameter( - name: RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMER_HEADER_KEY, - description: "Select a custom Transformer if needed", - in: "header", - allowEmptyValue: true, - schema: new OAT\Schema(enum: RESOURCE_{{ capitalsResource }}_DETAIL_TRANSFORMERS), - ), - // (TENANT_AWARENESS_{{ capitalsResource }}) ? new OAT\Parameter(ref: "#/components/parameters/tenant") : null, (API_VERSION_URL_QUERY) ? new OAT\Parameter( @@ -146,10 +138,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); schema: new OAT\Schema(type: "string") ) : null, - (API_VERSION_URL_HEADERS) ? new OAT\Parameter( + (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, diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 3b21d95..cda5553 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -129,14 +129,6 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); security: (!RESOURCE_PUBLIC_{{ capitalsResource }}_PAGINATION) ? [["bearerAuth" => []]] : null, parameters: [ - new OAT\HeaderParameter( - name: RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMER_HEADER_KEY, - description: "Select a custom Transformer if needed", - in: "header", - allowEmptyValue: true, - schema: new OAT\Schema(enum: RESOURCE_{{ capitalsResource }}_PAGINATION_TRANSFORMERS), - ), - // (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, @@ -148,10 +140,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); schema: new OAT\Schema(type: "string") ) : null, - (API_VERSION_URL_HEADERS) ? new OAT\Parameter( + (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, diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index 161239d..7884d3e 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -126,10 +126,11 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); schema: new OAT\Schema(type: "string") ) : null, - (API_VERSION_URL_HEADERS) ? new OAT\Parameter( + (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, From 8bcd6df476cd9e0adb87ead91a624caaf1335511 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 22:56:05 +0200 Subject: [PATCH 57/63] all CONST for versioning need to defined in all cases... --- stubs/Api/CreateHandler.stub | 6 ++++++ stubs/Api/DeleteHandler.stub | 6 ++++++ stubs/Api/DetailHandler.stub | 6 ++++++ stubs/Api/PaginationHandler.stub | 6 ++++++ stubs/Api/UpdateHandler.stub | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index 7e94c97..cd18e34 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -22,13 +22,19 @@ defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('ap 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); } diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index 5a45458..dffe99a 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -22,13 +22,19 @@ defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('ap 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); } diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index c4322a1..b5913c1 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -22,13 +22,19 @@ defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('ap 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); } diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index cda5553..38b863c 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -23,13 +23,19 @@ defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('ap 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); } diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index 7884d3e..f697f5d 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -22,13 +22,19 @@ defined("API_TRANSFORMER_HEADER") ?: define("API_TRANSFORMER_HEADER", config('ap 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); } From e3871a13917c97ea9d7f2be95bab98f11e8da454 Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 23:04:54 +0200 Subject: [PATCH 58/63] set default config vars because if new config is not published composer dumpautoload error --- src/ApiService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ApiService.php b/src/ApiService.php index 4662fa5..790301a 100644 --- a/src/ApiService.php +++ b/src/ApiService.php @@ -104,11 +104,11 @@ public static function getDefaultTransformerName(): string public static function getApiVersionMethod(): string { - return config('api-service.route.api_version_method'); + return config('api-service.route.api_version_method', 'headers'); } public static function getApiVersionParameterName(): string { - return config('api-service.route.api_version_parameter_name'); + return config('api-service.route.api_version_parameter_name', 'version'); } } From 5786107bd0f35801e9fd68bda3d8953cf9d3bd5e Mon Sep 17 00:00:00 2001 From: Eelco Date: Sun, 18 Aug 2024 23:15:29 +0200 Subject: [PATCH 59/63] return array should not be flipped in getApiTransformers() --- src/Http/Handlers.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 28476bf..b649365 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -124,10 +124,10 @@ public static function getApiTransformers(): array return array_merge([ ApiService::getDefaultTransformerName() => DefaultTransformer::class, ], method_exists(static::$resource, 'apiTransformers') ? - array_flip(array_combine( - array_map(fn ($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), - array_keys($transformers) - )) : []); // @phpstan-ignore-line + array_combine( + array_map(fn($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), + array_values($transformers) + ) : []); // @phpstan-ignore-line } /** From 839b92e1d599cff83e6a813185e9a446811e90ad Mon Sep 17 00:00:00 2001 From: Eelco Date: Mon, 19 Aug 2024 00:17:17 +0200 Subject: [PATCH 60/63] syntax fixes in api docs generation, and better prefix handling for Routes generation. {version} completely in the front of the url --- routes/api.php | 18 +++++++++--------- src/ApiService.php | 10 +++++++--- src/ApiServicePlugin.php | 4 ++-- stubs/Api/CreateHandler.stub | 4 ++-- stubs/Api/DeleteHandler.stub | 4 ++-- stubs/Api/DetailHandler.stub | 4 ++-- stubs/Api/PaginationHandler.stub | 4 ++-- stubs/Api/UpdateHandler.stub | 4 ++-- 8 files changed, 28 insertions(+), 24 deletions(-) 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 790301a..93f310d 100644 --- a/src/ApiService.php +++ b/src/ApiService.php @@ -35,18 +35,22 @@ public static function getResource() return static::$resource; } - public static function registerRoutes(Panel $panel) + public static function registerRoutes(Panel $panel, string $baseRoutePrefix) { - $versionPrefix = ''; + $versionPrefix = $baseRoutePrefix; + + $slug = static::getResource()::getSlug(); if (static::getApiVersionMethod() === 'path') { - $transformers = static::getResource()::getApiTransformers(); + $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('/', '.') 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/stubs/Api/CreateHandler.stub b/stubs/Api/CreateHandler.stub index cd18e34..ae4ff3e 100644 --- a/stubs/Api/CreateHandler.stub +++ b/stubs/Api/CreateHandler.stub @@ -104,7 +104,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) : @@ -117,7 +117,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) diff --git a/stubs/Api/DeleteHandler.stub b/stubs/Api/DeleteHandler.stub index dffe99a..966bd99 100644 --- a/stubs/Api/DeleteHandler.stub +++ b/stubs/Api/DeleteHandler.stub @@ -102,7 +102,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) : @@ -115,7 +115,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) diff --git a/stubs/Api/DetailHandler.stub b/stubs/Api/DetailHandler.stub index b5913c1..3888501 100644 --- a/stubs/Api/DetailHandler.stub +++ b/stubs/Api/DetailHandler.stub @@ -113,7 +113,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) : @@ -126,7 +126,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) diff --git a/stubs/Api/PaginationHandler.stub b/stubs/Api/PaginationHandler.stub index 38b863c..e64f485 100644 --- a/stubs/Api/PaginationHandler.stub +++ b/stubs/Api/PaginationHandler.stub @@ -112,7 +112,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) : @@ -125,7 +125,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) diff --git a/stubs/Api/UpdateHandler.stub b/stubs/Api/UpdateHandler.stub index f697f5d..7eeb0ae 100644 --- a/stubs/Api/UpdateHandler.stub +++ b/stubs/Api/UpdateHandler.stub @@ -102,7 +102,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) : @@ -115,7 +115,7 @@ defined("BASE_URL") ?: define("BASE_URL", config('app.url')); serverVariable: "version", description: 'Version or Transformer name of this API endpoint', default: API_DEFAULT_TRANSFORMER_NAME, - ), + ) : null, ] ) From 0f5babc8e63f9af70614615b4db8e35d35b4ce44 Mon Sep 17 00:00:00 2001 From: Eelco Date: Mon, 19 Aug 2024 00:24:29 +0200 Subject: [PATCH 61/63] removed incorrect array_keys() --- src/Http/Handlers.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index b649365..31866b3 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -138,7 +138,7 @@ protected static function getTransformerFromUrlPath(): string $routeApiVersion = request()->route(ApiService::getApiVersionParameterName()); $transformer = Str::kebab($routeApiVersion); - if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { throw new TransformerNotFoundException($transformer); } @@ -162,7 +162,7 @@ protected static function getTransformerFromUrlQuery(): string $transformer = request()->input($queryName); $transformer = Str::kebab($transformer); - if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { throw new TransformerNotFoundException($transformer); } @@ -186,7 +186,7 @@ protected static function getTransformerFromRequestHeader(): string $transformer = request()->headers->get($headerName); $transformer = Str::kebab($transformer); - if ($transformer && !array_key_exists($transformer, array_keys(self::getApiTransformers()))) { + if ($transformer && !array_key_exists($transformer, self::getApiTransformers())) { throw new TransformerNotFoundException($transformer); } From 8023f8682caf5af3d697960290809ff07cb7b0f4 Mon Sep 17 00:00:00 2001 From: Eelco Date: Mon, 19 Aug 2024 00:29:47 +0200 Subject: [PATCH 62/63] small fix in getApiTransformers() --- src/Http/Handlers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 31866b3..1eec832 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -125,8 +125,8 @@ public static function getApiTransformers(): array ApiService::getDefaultTransformerName() => DefaultTransformer::class, ], method_exists(static::$resource, 'apiTransformers') ? array_combine( - array_map(fn($class) => Str::kebab(class_basename($class)), $transformers = static::$resource::apiTransformers()), - array_values($transformers) + array_map(fn($class) => Str::kebab(class_basename($class)), $transformers = array_flip(static::$resource::apiTransformers())), + array_keys($transformers) ) : []); // @phpstan-ignore-line } From d901c1dc29c0de6b105d6b94e11c616c6e62bac2 Mon Sep 17 00:00:00 2001 From: Eelco Date: Tue, 20 Aug 2024 14:13:40 +0200 Subject: [PATCH 63/63] typo in match() case --- src/Http/Handlers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Handlers.php b/src/Http/Handlers.php index 1eec832..6809bac 100644 --- a/src/Http/Handlers.php +++ b/src/Http/Handlers.php @@ -112,7 +112,7 @@ public static function getApiTransformer(): ?string return match (ApiService::getApiVersionMethod()) { 'path' => static::getTransformerFromUrlPath(), 'query' => static::getTransformerFromUrlQuery(), - 'header' => static::getTransformerFromRequestHeader(), + 'headers' => static::getTransformerFromRequestHeader(), }; }