Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature version support via transformers #69

Open
wants to merge 72 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
691e9b8
Merge pull request #1 from eelco2k/feature-tenantAware
eelco2k Mar 5, 2024
774bfbf
Merge branch 'rupadana:main' into main
eelco2k Mar 19, 2024
208e601
Merge branch 'rupadana:main' into main
eelco2k Apr 2, 2024
ababe8c
first work on api docs generation with command and stubs
Apr 3, 2024
1588254
basic explanation for Swagger Api Docs generation
Apr 3, 2024
7f6c761
more updated readme
Apr 3, 2024
ae2504d
removed unused $panel parameter
Apr 3, 2024
8251ece
wrong namespace
Apr 3, 2024
9cf45dc
cleanup unused classes
Apr 3, 2024
8159fb5
small changes cleanup
Apr 3, 2024
0f81ae7
add command to ApiServiceProvider getCommands() function
Apr 3, 2024
74cbcfe
added correct namespace for Transformer stub
Apr 3, 2024
6080f4f
fixes from @POMXARK, OAT/Xml and replace App to app.
May 9, 2024
79bf39c
fixes from @POMXARK, OAT/Xml and replace App to app.
May 9, 2024
bf0dc5c
small readme change
May 9, 2024
fa0df1f
use str()->kebab() for path.
May 24, 2024
e13db06
updated README for required transformer when using API Generator
May 24, 2024
d79afae
better Transformer API Generator readme
May 24, 2024
161f21e
readme php syntax
May 24, 2024
af32378
format readme
May 24, 2024
47246bb
format readme
May 24, 2024
8bfafbd
typo readme
May 24, 2024
596291e
Update README.md
rupadana Jun 7, 2024
c27d69f
Fixes some situations where default Panel is not an Tenancy enabled p…
Jun 7, 2024
b14fad5
base codework for multiple transformers via HTTP Headers
Jun 11, 2024
5885fa4
Added updated readme for multiple handlers, and fallback to default h…
Jun 11, 2024
82a1633
merge commits
Jun 11, 2024
1600844
Merge pull request #3 from rupadana/main
eelco2k Jun 11, 2024
abd2d58
reverted the missing trait in handlers
Jun 11, 2024
799706d
ability to select the created custom Transformers in the api Docs
Jun 20, 2024
a99eeb6
small fix in duplicate overridden variable
Jun 20, 2024
355bc14
detail and pagination handler now retreive all the Transformers, and …
Jun 20, 2024
84eab67
command generating api docs now also generates the defaultTransformer…
Jun 20, 2024
158308a
typo in variable name
Jun 20, 2024
9fb0237
requests and responses of Api request can always be the custom or de …
Jun 20, 2024
dc0f59d
small issue in requestBody return schema
Jun 20, 2024
094a4a4
auto generate DTO properies wehn available from laravel-data package.…
Jul 24, 2024
46a7477
small fix in stubVars when no DTO is found on the Resource
Jul 24, 2024
82ccf51
small change in code
Jul 25, 2024
c697d40
allow dynamic properties added to the #[ApiPropertyInfo( ....) Attr…
Jul 25, 2024
287098b
ApiPropertyInfo now has AllowDynamicProperties for handling dynamic A…
Jul 25, 2024
5ce8931
adding examples in the readme for extra optional Attribute fields
Jul 25, 2024
431335c
move the laravel-data readme part to the bottom
Jul 25, 2024
06f2873
remove some empty line endings
Jul 25, 2024
d25b2c6
small readme styling optimizations
Jul 25, 2024
8a4276a
typehint param in constructor
Jul 25, 2024
9a45e79
correct formatting of the extraProperties
Jul 25, 2024
fe962f4
read property types from OpenApi\Attributes $_types array. and use th…
Jul 25, 2024
279c70d
remove title: as a base / static attribute and make it dynamic via th…
Jul 25, 2024
4469009
make sure array and object types get the extra OAT\Items() and schema…
Jul 26, 2024
361c5c4
initial setup for version support via URL path/query/header
Aug 18, 2024
b0d3a1e
Merge remote-tracking branch 'origin/main' into feature-version-suppo…
Aug 18, 2024
bb10b28
Merge pull request #5 from rupadana/main
eelco2k Aug 18, 2024
2eb2a81
getApiTransformers() now with key -> values so needed to change this …
Aug 18, 2024
c504b06
Merge pull request #6 from rupadana/main
eelco2k Aug 18, 2024
31a832c
Merge remote-tracking branch 'origin/main' into feature-version-suppo…
Aug 18, 2024
56dbdbf
typo and text changed in readme.md
Aug 18, 2024
dc316f9
Cherry pick Readme.md from main branch
eelco2k Aug 18, 2024
9488241
add apiTransformers() to resource stub. update readme about this func…
eelco2k Aug 18, 2024
292b1cc
Revert "add apiTransformers() to resource stub. update readme about t…
eelco2k Aug 18, 2024
8a7392c
readme update
eelco2k Aug 18, 2024
a2d57ed
reflect published config in readme.md
eelco2k Aug 18, 2024
fea4cf3
added basic api docs variables for the three methods of API versioning.
eelco2k Aug 18, 2024
25ed406
when version via HTTP Header then its a OAT\HeaderParameter()
eelco2k Aug 18, 2024
8bcd6df
all CONST for versioning need to defined in all cases...
eelco2k Aug 18, 2024
e3871a1
set default config vars because if new config is not published compos…
eelco2k Aug 18, 2024
5786107
return array should not be flipped in getApiTransformers()
eelco2k Aug 18, 2024
839b92e
syntax fixes in api docs generation, and better prefix handling for R…
eelco2k Aug 18, 2024
0f5babc
removed incorrect array_keys()
eelco2k Aug 18, 2024
8023f86
small fix in getApiTransformers()
eelco2k Aug 18, 2024
d901c1d
typo in match() case
eelco2k Aug 20, 2024
dd7cc42
Merge branch 'main' into feature-version-support-via-transformers
rupadana Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 245 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -98,7 +102,6 @@ Token Resource is protected by TokenPolicy. You can disable it by publishing the
### Filtering & Allowed Field

We used `"spatie/laravel-query-builder": "^5.3"` to handle query selecting, sorting and filtering. Check out [the spatie/laravel-query-builder documentation](https://spatie.be/docs/laravel-query-builder/v5/introduction) for more information.

In order to allow modifying the query for your model you can implement the `HasAllowedFields`, `HasAllowedSorts` and `HasAllowedFilters` Contracts in your model.

```php
Expand Down Expand Up @@ -178,15 +181,106 @@ 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;
}
...
}
```

### 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<string, string>
*/
public static function apiTransformers(): array
{
return
[
'blog' => BlogTransformer::class,
'mod-blog' => ModifiedBlogTransformer::class,
'extra-blog-column' => ExtraBlogColumnsTransformer::class,
... etc.
]
}
...
}

```

Now you can use the extra HTTP HEADER `X-API-TRANSFORMER` in your request with the name of the transformer class.

Here an example in guzzle:

```php
$client->request('GET', '/api/blogs', [
'headers' => [
'X-API-TRANSFORMER' => 'ModifiedBlogTransformer'
]
]);

or

$client->request('GET', '/api/blogs', [
'headers' => [
'X-API-TRANSFORMER' => 'ExtraBlogColumnsTransformer'
]
]);
```

This way the correct transformer will be used to give you the correct response json.

### Multiple Transformers via URL Path or URL Query

Alternative methods could also be used, like using as a prefix in the URL path or in the URL Query.

You can set the method in the config `route.api_version_method` to 'path' or 'query'. You can also set the default Transformername via `route.default_transformer_name` (defaults to 'default').

When you set `route.api_version_method` to 'path' then you can use the name of the transformer in the first segment of the API URL. the name of that fist segment is the key which you have defined in the `apiTransformers()` function.

So for example if you want to use the `'mod-blog'` transformer in your api response. the url might look like this:
'<https://myproject.com/api/`mod-blog`/blog/1>'

It will always be in front of all other options like `tenant` or `panel` names in the url.

With this method you could use API versioning like `/api/v1` where `v1` is an item in the `apiTransformers()` function.

like so:

```php

public static function apiTransformers(): array
{
return
[
'v1' => VersionOneTransformer::class,
... etc.
]
}
```

Another method is using `route.api_version_method` to 'query'. This way you can add an extra parameter in the URL with the name which you defined in your config under `route.api_version_parameter_name`
by default this parameter is `version`. With this config the URL would look like this:
'<https://myproject.com/api/blog/1?version=v1>'

IMPORTANT: You can only use one method for this package for API versioning.

### Group Name & Prefix

You can edit prefix & group route name as you want, default this plugin use model singular label;
Expand Down Expand Up @@ -270,6 +364,154 @@ class PaginationHandler extends Handlers {
}
```

## Swagger Api Docs Generation

It is possible to generate Swagger API docs with this package. You have to make sure you have the following dependencies:

```bash
composer require darkaonline/l5-swagger
```

And make sure you install it correctly according to their [installation manual](https://github.com/DarkaOnLine/L5-Swagger/wiki/Installation-&-Configuration#installation).
In development we recommend setting the config in `l5-swagger.php` `defaults.generate_always` to `true`.

When generating Api Swagger Docs for an Filament Resource it is required to define a Transformer. Otherwise the generator does not know how your resource entity types are being handled. What the response format and types look like.

Therefor you should always create a Transformer, which is explained above at the section [Transform API Response](#transform-api-response).

Then you can use the following command to generate API docs for your resources:

```bash
php artisan make:filament-api-docs {resource?} {namespace?}
```

so for example:

```bash
php artisan make:filament-api-docs BlogResource
```

The CLI command accepts two optional parameters: `{resource?}` and `{namespace?}`.
By default the Swagger API Docs will be placed in `app/Virtual/Filament/Resources` folder under their own resource name.

For example the BlogResource Api Docs wil be in the following folder `app/Virtual/Filament/Resource/BlogResource`.

First it will check if you have an existing the Swagger Docs Server config. This is a file `ApiDocsController.php` and resides in `app/Virtual/Filament/Resources`.
It holds some general information about your swagger API Docs server. All generated files can be manual edited afterwards.
Regenerating an API Docs Serverinfo or Resource will always ask you if you want to override the existing file.

When done, you can go to the defined swagger documentation URL as defined in `l5-swagger.php` config as `documentations.routes.api`.

If you want to generate the Api Docs manually because in your `l5-swagger.php` config you have set `defatuls.generate_always` to `false` you can do so by invoking:

```bash
php artisan l5-swagger:generate
```

After you have generated the Swagger API Docs, you can add your required Transformer properties as needed.

by default as an example it will generate this when you use a BlogTransformer:

```php
class BlogTransformer {

#[OAT\Property(
property: "data",
type: "array",
items: new OAT\Items(
properties: [
new OAT\Property(property: "id", type: "integer", title: "ID", description: "id of the blog", example: ""),

// Add your own properties corresponding to the BlogTransformer
]
),
)]
public $data;

...
}

```

You can find more about all possible properties at <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 in the `extraProperties` parameter. See the last item in the example.
So this is how you can implement those:

```php

namespace App\Data;

use Rupadana\ApiService\Attributes\ApiPropertyInfo; // <-- add this Attribute to you DTO
use Spatie\LaravelData\Data;

class BlogData extends Data
{
public function __construct(
#[ApiPropertyInfo(description: 'ID of the Blog DTO', example: '')]
public ?int $id,
#[ApiPropertyInfo(description: 'Name of the Blog', example: '')]
public string $name,
#[ApiPropertyInfo(description: 'Image Url of the Blog', example: '', ref: "ImageBlogSchema", oneOf: '[new OAT\Schema(type:"string"), new OAT\Schema(type:"integer")]']
)]
public string $image,
) {
}
}

```

As you can see you can add attributes above each property. this way when generating the transformer Api Docs it will add these information.

The result of the Api Docs generation of the Transformer(s) will look like this:

```php
namespace App\Virtual\Filament\Resources\BlogResource\Transformers;


use OpenApi\Attributes as OAT;

#[OAT\Schema(
schema: "BlogTransformer",
title: "BlogTransformer",
description: "Brands API Transformer",
xml: new OAT\Xml(name: "BlogTransformer"),
)]

class BlogTransformer {

#[OAT\Property(
property: "data",
type: "array",
items: new OAT\Items(
properties: [
new OAT\Property(property: 'id', type: 'int', title: 'id', description: 'ID of the Blog DTO', example: ''),
new OAT\Property(property: 'name', type: 'string', title: 'name', description: 'Name of the Blog', example: ''),
new OAT\Property(
property: 'image',
type: 'string',
title: 'image',
description: 'Image Url of the Blog',
example: '',
ref: "MyBlogSchema",
oneOf: [
new OAT\Schema(type="string"),
new OAT\Schema(type="integer")
]),
]
),
)],
public $data;
...
```

## License

The MIT License (MIT).
Expand Down
8 changes: 7 additions & 1 deletion config/api-service.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

return [
'navigation' => [
'token' => [
Expand All @@ -17,9 +19,13 @@
'route' => [
'panel_prefix' => true,
'use_resource_middlewares' => false,
'default_transformer_name' => 'default',
'api_version_method' => 'headers', // options: ['path', 'query', 'headers']
'api_version_parameter_name' => env('API_VERSION_PARAMETER_NAME', 'version'),
'api_transformer_header' => env('API_TRANSFORMER_HEADER', 'X-API-TRANSFORMER'),
],
'tenancy' => [
'enabled' => false,
'awareness' => false,
],
]
];
18 changes: 9 additions & 9 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,29 @@
$tenantSlugAttribute = $panel->getTenantSlugAttribute();
$apiServicePlugin = $panel->getPlugin('api-service');
$middlewares = $apiServicePlugin->getMiddlewares();
$panelRoutePrefix = ApiService::isRoutePrefixedByPanel() ? '{panel}' : '';
$panelRoutePrefix = ApiService::isRoutePrefixedByPanel() ? '{panel}/' : '';
$panelNamePrefix = $panelRoutePrefix ? $panel->getId() . '.' : '';

if (
$hasTenancy &&
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) {
Expand Down
Loading
Loading