Skip to content

IBX-8190: Update REST new resource #2682

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

Open
wants to merge 30 commits into
base: 5.0
Choose a base branch
from
Open

IBX-8190: Update REST new resource #2682

wants to merge 30 commits into from

Conversation

adriendupuis
Copy link
Contributor

@adriendupuis adriendupuis commented Mar 27, 2025

Question Answer
JIRA Ticket IBX-8190
Versions
Edition
  • Use serialiser/normalizer/denormalizer instead of ValueObjectVisitor/InputParser.
  • Add GET /greet and POST /greet to schema/doc (/api/ibexa/v2/doc#/App/api_greet_get).

Preview: Creating new REST resource

Checklist

  • Text renders correctly
  • Text has been checked with vale
  • Description metadata is up to date
  • Redirects cover removed/moved pages
  • Code samples are working
  • PHP code samples have been fixed with PHP CS fixer
  • Added link to this PR in relevant JIRA ticket or code PR

Copy link

github-actions bot commented Mar 27, 2025

Preview of modified Markdown:

adriendupuis and others added 7 commits April 2, 2025 09:09
Fix the following for low coast
Parameter #1 $string of function substr expects string, string|false given.
Try to avoid "Call to function is_array() with array will always evaluate to true."
Comment on lines +16 to +18
if ('json' === $format) {
$data = $data[array_key_first($data)];
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrap JSON.
I could even test that 'GreetingInput' === array_key_first($data)

@adriendupuis adriendupuis changed the title IBX-8190: Update REST customization IBX-8190: Update REST new resource Apr 2, 2025
@mnocon mnocon changed the base branch from master to 5.0 May 19, 2025 07:25
# Conflicts:
#	code_samples/api/rest_api/src/Rest/InputParser/GreetingInput.php
#	phpstan-baseline.neon
Copy link

Preview of modified files

Preview of modified Markdown:

`Compile Error: Declaration of App\Rest\Serializer\GreetingInputDenormalizer::denormalize(mixed $data, string $type, ?string $format = null, array $context = []) must be compatible with Symfony\Component\Serializer\Normalizer\DenormalizerInterface::denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed`
`Compile Error: Declaration of App\Rest\Serializer\GreetingInputDenormalizer::supportsDenormalization(mixed $data, string $type, ?string $format = null): bool must be compatible with Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool`
`Error: Class App\Rest\Serializer\GreetingInputDenormalizer contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Symfony\Component\Serializer\Normalizer\DenormalizerInterface::getSupportedTypes)`
`Compile Error: Declaration of App\Rest\Serializer\GreetingNormalizer::normalize(mixed $object, ?string $format = null, array $context = []): mixed must be compatible with Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize(mixed $data, ?string $format = null, array $context = []): ArrayObject|array|string|int|float|bool|null`
`Compile Error: Declaration of App\Rest\Serializer\GreetingNormalizer::supportsNormalization(mixed $data, ?string $format = null) must be compatible with Symfony\Component\Serializer\Normalizer\NormalizerInterface::supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool`
`Error: Class App\Rest\Serializer\GreetingNormalizer contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Symfony\Component\Serializer\Normalizer\NormalizerInterface::getSupportedTypes)`
Copy link

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/api/rest_api/config/routes_rest.yaml

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@29:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@30:[[= include_file('code_samples/api/rest_api/config/routes_rest.yaml', 0, 3) =]] methods: [GET]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@31:```

001⫶app.rest.greeting:
002⫶ path: '/greet'

code_samples/api/rest_api/config/routes_rest.yaml

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@29:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@30:[[= include_file('code_samples/api/rest_api/config/routes_rest.yaml', 0, 3) =]] methods: [GET]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@31:```

001⫶app.rest.greeting:
002⫶ path: '/greet'
003⫶    controller: App\Rest\Controller\DefaultController::helloWorld
003⫶    controller: App\Rest\Controller\DefaultController::greet
004⫶    methods: [GET]

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@38:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@39:[[= include_file('code_samples/api/rest_api/config/routes_rest.yaml') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@40:```

001⫶app.rest.greeting:
002⫶ path: '/greet'
004⫶    methods: [GET]

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@38:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@39:[[= include_file('code_samples/api/rest_api/config/routes_rest.yaml') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@40:```

001⫶app.rest.greeting:
002⫶ path: '/greet'
003⫶    controller: App\Rest\Controller\DefaultController::helloWorld
003⫶    controller: App\Rest\Controller\DefaultController::greet
004⫶    methods: [GET,POST]
005⫶ defaults:
006⫶ csrf_protection: false


code_samples/api/rest_api/config/services.yaml

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@41:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@42:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@43: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@44:[[= include_file('code_samples/api/rest_api/config/services.yaml', 28, 35) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@45:```

001⫶services:
002⫶ #…
003⫶ App\Rest\ValueObjectVisitor\RestLocation:
004⫶ class: App\Rest\ValueObjectVisitor\RestLocation
005⫶ parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
006⫶ arguments:
007⫶ $urlAliasService: '@ibexa.api.service.url_alias'
008⫶ tags:
009⫶ - { name: app.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\RestLocation }

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@52:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@53:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@54: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@55:[[= include_file('code_samples/api/rest_api/config/services.yaml', 22, 27) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@56:```

001⫶services:
002⫶ #…
003⫶ App\Rest\Output\ValueObjectVisitorDispatcher:
004⫶ class: App\Rest\Output\ValueObjectVisitorDispatcher
005⫶ arguments:
006⫶ - !tagged_iterator { tag: 'app.rest.output.value_object.visitor', index_by: 'type' }
007⫶ - '@Ibexa\Contracts\Rest\Output\ValueObjectVisitorDispatcher'

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@67:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@68:parameters:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@69: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@70:[[= include_file('code_samples/api/rest_api/config/services.yaml', 1, 3) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@71:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@72: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@73:[[= include_file('code_samples/api/rest_api/config/services.yaml', 6, 21) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@74:```

001⫶parameters:
002⫶ #…
003⫶ app.rest.output.visitor.xml.regexps: ['(^application/app\.api\.[A-Za-z]+\+xml$)']
004⫶ app.rest.output.visitor.json.regexps: ['(^application/app\.api\.[A-Za-z]+\+json$)']
005⫶
006⫶services:
007⫶ #…
008⫶ app.rest.output.visitor.xml:
009⫶ class: Ibexa\Contracts\Rest\Output\Visitor
010⫶ arguments:
011⫶ - '@Ibexa\Rest\Output\Generator\Xml'
012⫶ - '@App\Rest\Output\ValueObjectVisitorDispatcher'
013⫶ tags:
014⫶ - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.xml.regexps, priority: 20 }
015⫶
016⫶ app.rest.output.visitor.json:
017⫶ class: Ibexa\Contracts\Rest\Output\Visitor
018⫶ arguments:
019⫶ - '@Ibexa\Rest\Output\Generator\Json'
020⫶ - '@App\Rest\Output\ValueObjectVisitorDispatcher'
021⫶ tags:
022⫶ - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.json.regexps, priority: 20 }

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@48:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@49:services:
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@50: #…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@51:[[= include_file('code_samples/api/rest_api/config/services.yaml', 36, 42) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@52:```

001⫶services:
002⫶ #…
003⫶ App\Rest\Controller\:
004⫶ resource: '../src/Rest/Controller/'
005⫶ parent: Ibexa\Rest\Server\Controller
006⫶ autowire: true
007⫶ autoconfigure: true
004⫶    methods: [GET,POST]
005⫶ defaults:
006⫶ csrf_protection: false


code_samples/api/rest_api/config/services.yaml

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@41:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@42:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@43: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@44:[[= include_file('code_samples/api/rest_api/config/services.yaml', 28, 35) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@45:```

001⫶services:
002⫶ #…
003⫶ App\Rest\ValueObjectVisitor\RestLocation:
004⫶ class: App\Rest\ValueObjectVisitor\RestLocation
005⫶ parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
006⫶ arguments:
007⫶ $urlAliasService: '@ibexa.api.service.url_alias'
008⫶ tags:
009⫶ - { name: app.rest.output.value_object.visitor, type: Ibexa\Rest\Server\Values\RestLocation }

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@52:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@53:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@54: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@55:[[= include_file('code_samples/api/rest_api/config/services.yaml', 22, 27) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@56:```

001⫶services:
002⫶ #…
003⫶ App\Rest\Output\ValueObjectVisitorDispatcher:
004⫶ class: App\Rest\Output\ValueObjectVisitorDispatcher
005⫶ arguments:
006⫶ - !tagged_iterator { tag: 'app.rest.output.value_object.visitor', index_by: 'type' }
007⫶ - '@Ibexa\Contracts\Rest\Output\ValueObjectVisitorDispatcher'

docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@67:``` yaml
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@68:parameters:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@69: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@70:[[= include_file('code_samples/api/rest_api/config/services.yaml', 1, 3) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@71:services:
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@72: #…
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@73:[[= include_file('code_samples/api/rest_api/config/services.yaml', 6, 21) =]]
docs/api/rest_api/extending_rest_api/adding_custom_media_type.md@74:```

001⫶parameters:
002⫶ #…
003⫶ app.rest.output.visitor.xml.regexps: ['(^application/app\.api\.[A-Za-z]+\+xml$)']
004⫶ app.rest.output.visitor.json.regexps: ['(^application/app\.api\.[A-Za-z]+\+json$)']
005⫶
006⫶services:
007⫶ #…
008⫶ app.rest.output.visitor.xml:
009⫶ class: Ibexa\Contracts\Rest\Output\Visitor
010⫶ arguments:
011⫶ - '@Ibexa\Rest\Output\Generator\Xml'
012⫶ - '@App\Rest\Output\ValueObjectVisitorDispatcher'
013⫶ tags:
014⫶ - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.xml.regexps, priority: 20 }
015⫶
016⫶ app.rest.output.visitor.json:
017⫶ class: Ibexa\Contracts\Rest\Output\Visitor
018⫶ arguments:
019⫶ - '@Ibexa\Rest\Output\Generator\Json'
020⫶ - '@App\Rest\Output\ValueObjectVisitorDispatcher'
021⫶ tags:
022⫶ - { name: ibexa.rest.output.visitor, regexps: app.rest.output.visitor.json.regexps, priority: 20 }

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@48:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@49:services:
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@50: #…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@51:[[= include_file('code_samples/api/rest_api/config/services.yaml', 36, 42) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@52:```

001⫶services:
002⫶ #…
003⫶ App\Rest\Controller\:
004⫶ resource: '../src/Rest/Controller/'
005⫶ parent: Ibexa\Rest\Server\Controller
006⫶ autowire: true
007⫶ autoconfigure: true
008⫶        tags: [ 'controller.service_arguments' ]
008⫶        tags: [ 'controller.service_arguments', 'ibexa.api_platform.resource' ]


docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@98:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@99:services:
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@100: #…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@101:[[= include_file('code_samples/api/rest_api/config/services.yaml', 43, 48) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@102:```
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@93:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@94:services:
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@95: #…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@96:[[= include_file('code_samples/api/rest_api/config/services.yaml', 43, 48) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@97:```

001⫶services:
002⫶ #…

001⫶services:
002⫶ #…
003⫶    App\Rest\ValueObjectVisitor\Greeting:
004⫶ parent: Ibexa\Contracts\Rest\Output\ValueObjectVisitor
005⫶ tags:
006⫶ - { name: ibexa.rest.output.value_object.visitor, type: App\Rest\Values\Greeting }

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@120:``` yaml
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@121:services:
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@122: #…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@123:[[= include_file('code_samples/api/rest_api/config/services.yaml', 48, 53) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@124:```

001⫶services:
002⫶ #…
003⫶ App\Rest\InputParser\GreetingInput:
004⫶ parent: Ibexa\Rest\Server\Common\Parser
005⫶ tags:
006⫶ - { name: ibexa.rest.input.parser, mediaType: application/vnd.ibexa.api.GreetingInput }
003⫶
004⫶ App\Rest\Serializer\:
005⫶ resource: '../src/Rest/Serializer/'
006⫶ tags: ['ibexa.rest.serializer.normalizer']


code_samples/api/rest_api/src/Rest/Controller/DefaultController.php



code_samples/api/rest_api/src/Rest/Controller/DefaultController.php

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@63:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@64:[[= include_file('code_samples/api/rest_api/src/Rest/Controller/DefaultController.php') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@65:```
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@66:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@67:[[= include_file('code_samples/api/rest_api/src/Rest/Controller/DefaultController.php', 0, 14) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@68:[[= include_file('code_samples/api/rest_api/src/Rest/Controller/DefaultController.php', 246) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@69:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Rest\Controller;
004⫶

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Rest\Controller;
004⫶
005⫶use App\Rest\Values\Greeting;
006⫶use Ibexa\Rest\Message;
007⫶use Ibexa\Rest\Server\Controller;
008⫶use Symfony\Component\HttpFoundation\Request;
009⫶
010⫶class DefaultController extends Controller
011⫶{
012⫶ public function greet(Request $request): Greeting
013⫶ {
014⫶ if ('POST' === $request->getMethod()) {
015⫶ return $this->inputDispatcher->parse(
016⫶ new Message(
017⫶ ['Content-Type' => $request->headers->get('Content-Type')],
018⫶ $request->getContent()
019⫶ )
020⫶ );
021⫶ }
022⫶
023⫶ return new Greeting();
024⫶ }
025⫶}
005⫶use ApiPlatform\Metadata\Get;
006⫶use ApiPlatform\Metadata\Post;
007⫶use ApiPlatform\OpenApi\Factory\OpenApiFactory;
008⫶use ApiPlatform\OpenApi\Model;
009⫶use App\Rest\Values\Greeting;
010⫶use Ibexa\Rest\Server\Controller;
011⫶use Symfony\Component\HttpFoundation\Request;
012⫶use Symfony\Component\HttpFoundation\Response;
013⫶use Symfony\Component\Serializer\Encoder\XmlEncoder;
014⫶use Symfony\Component\Serializer\SerializerInterface;

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@169:``` php hl_lines="5 6 16 100"
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@170:[[= include_file('code_samples/api/rest_api/src/Rest/Controller/DefaultController.php', 0, 247) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@171://…
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@172:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Rest\Controller;
004⫶
005❇️use ApiPlatform\Metadata\Get;
006❇️use ApiPlatform\Metadata\Post;
007⫶use ApiPlatform\OpenApi\Factory\OpenApiFactory;
008⫶use ApiPlatform\OpenApi\Model;
009⫶use App\Rest\Values\Greeting;
010⫶use Ibexa\Rest\Server\Controller;
011⫶use Symfony\Component\HttpFoundation\Request;
012⫶use Symfony\Component\HttpFoundation\Response;
013⫶use Symfony\Component\Serializer\Encoder\XmlEncoder;
014⫶use Symfony\Component\Serializer\SerializerInterface;
015⫶
016❇️#[Get(
017⫶ uriTemplate: '/greet',
018⫶ extraProperties: [OpenApiFactory::OVERRIDE_OPENAPI_RESPONSES => false],
019⫶ openapi: new Model\Operation(
020⫶ summary: 'Greet',
021⫶ description: 'Greets a recipient with a salutation',
022⫶ tags: [
023⫶ 'App',
024⫶ ],
025⫶ parameters: [
026⫶ new Model\Parameter(
027⫶ name: 'Accept',
028⫶ in: 'header',
029⫶ required: false,
030⫶ description: 'If set, the greeting is returned in XML or JSON format.',
031⫶ schema: [
032⫶ 'type' => 'string',
033⫶ ],
034⫶ example: 'application/vnd.ibexa.api.Greeting+json',
035⫶ ),
036⫶ ],
037⫶ responses: [
038⫶ Response::HTTP_OK => [
039⫶ 'description' => 'OK - Return a greeting',
040⫶ 'content' => [
041⫶ 'application/vnd.ibexa.api.Greeting+xml' => [
042⫶ 'schema' => [
043⫶ 'xml' => [
044⫶ 'name' => 'Greeting',
045⫶ 'wrapped' => false,
046⫶ ],
047⫶ 'properties' => [
048⫶ 'salutation' => [
049⫶ 'type' => 'string',
050⫶ ],
051⫶ 'recipient' => [
052⫶ 'type' => 'string',
053⫶ ],
054⫶ 'sentence' => [
055⫶ 'type' => 'string',
056⫶ 'description' => 'Composed sentence using salutation and recipient.',
057⫶ ],
058⫶ ],
059⫶ ],
060⫶ 'example' => [
061⫶ 'salutation' => 'Hello',
062⫶ 'recipient' => 'World',
063⫶ 'sentence' => 'Hello World',
064⫶ ],
065⫶ ],
066⫶ 'application/vnd.ibexa.api.Greeting+json' => [
067⫶ 'schema' => [
068⫶ 'type' => 'object',
069⫶ 'properties' => [
070⫶ 'Greeting' => [
071⫶ 'type' => 'object',
072⫶ 'properties' => [
073⫶ 'salutation' => [
074⫶ 'type' => 'string',
075⫶ ],
076⫶ 'recipient' => [
077⫶ 'type' => 'string',
078⫶ ],
079⫶ 'sentence' => [
080⫶ 'type' => 'string',
081⫶ 'description' => 'Composed sentence using salutation and recipient.',
082⫶ ],
083⫶ ],
084⫶ ],
085⫶ ],
086⫶ ],
087⫶ 'example' => [
088⫶ 'Greeting' => [
089⫶ 'salutation' => 'Hello',
090⫶ 'recipient' => 'World',
091⫶ 'sentence' => 'Hello World',
092⫶ ],
093⫶ ],
094⫶ ],
095⫶ ],
096⫶ ],
097⫶ ],
098⫶ ),
099⫶)]
100❇️#[Post(
101⫶ uriTemplate: '/greet',
102⫶ extraProperties: [OpenApiFactory::OVERRIDE_OPENAPI_RESPONSES => false],
103⫶ openapi: new Model\Operation(
104⫶ summary: 'Greet',
105⫶ description: 'Greets a recipient with a salutation',
106⫶ tags: [
107⫶ 'App',
108⫶ ],
109⫶ parameters: [
110⫶ new Model\Parameter(
111⫶ name: 'Content-Type',
112⫶ in: 'header',
113⫶ required: false,
114⫶ description: 'The greeting input schema encoded in XML or JSON.',
115⫶ schema: [
116⫶ 'type' => 'string',
117⫶ ],
118⫶ example: 'application/vnd.ibexa.api.GreetingInput+json',
119⫶ ),
120⫶ new Model\Parameter(
121⫶ name: 'Accept',
122⫶ in: 'header',
123⫶ required: false,
124⫶ description: 'If set, the greeting is returned in XML or JSON format.',
125⫶ schema: [
126⫶ 'type' => 'string',
127⫶ ],
128⫶ example: 'application/vnd.ibexa.api.Greeting+json',
129⫶ ),
130⫶ ],
131⫶ requestBody: new Model\RequestBody(
132⫶ required: false,
133⫶ content: new \ArrayObject([
134⫶ 'application/vnd.ibexa.api.GreetingInput+xml' => [
135⫶ 'schema' => [
136⫶ 'type' => 'object',
137⫶ 'xml' => [
138⫶ 'name' => 'GreetingInput',
139⫶ 'wrapped' => false,
140⫶ ],
141⫶ 'properties' => [
142⫶ 'salutation' => [
143⫶ 'type' => 'string',
144⫶ 'required' => false,
145⫶ ],
146⫶ 'recipient' => [
147⫶ 'type' => 'string',
148⫶ 'required' => false,
149⫶ ],
150⫶ ],
151⫶ ],
152⫶ 'example' => [
153⫶ 'salutation' => 'Good morning',
154⫶ ],
155⫶ ],
156⫶ 'application/vnd.ibexa.api.GreetingInput+json' => [
157⫶ 'schema' => [
158⫶ 'type' => 'object',
159⫶ 'properties' => [
160⫶ 'GreetingInput' => [
161⫶ 'type' => 'object',
162⫶ 'properties' => [
163⫶ 'salutation' => [
164⫶ 'type' => 'string',
165⫶ 'required' => false,
166⫶ ],
167⫶ 'recipient' => [
168⫶ 'type' => 'string',
169⫶ 'required' => false,
170⫶ ],
171⫶ ],
172⫶ ],
173⫶ ],
174⫶ ],
175⫶ 'example' => [
176⫶ 'GreetingInput' => [
177⫶ 'salutation' => 'Good day',
178⫶ 'recipient' => 'Earth',
179⫶ ],
180⫶ ],
181⫶ ],
182⫶ ]),
183⫶ ),
184⫶ responses: [
185⫶ Response::HTTP_OK => [
186⫶ 'description' => 'OK - Return a greeting',
187⫶ 'content' => [
188⫶ 'application/vnd.ibexa.api.Greeting+xml' => [
189⫶ 'schema' => [
190⫶ 'xml' => [
191⫶ 'name' => 'Greeting',
192⫶ 'wrapped' => false,
193⫶ ],
194⫶ 'properties' => [
195⫶ 'salutation' => [
196⫶ 'type' => 'string',
197⫶ ],
198⫶ 'recipient' => [
199⫶ 'type' => 'string',
200⫶ ],
201⫶ 'sentence' => [
202⫶ 'type' => 'string',
203⫶ 'description' => 'Composed sentence using salutation and recipient.',
204⫶ ],
205⫶ ],
206⫶ ],
207⫶ 'example' => [
208⫶ 'salutation' => 'Good morning',
209⫶ 'recipient' => 'World',
210⫶ 'sentence' => 'Good Morning World',
211⫶ ],
212⫶ ],
213⫶ 'application/vnd.ibexa.api.Greeting+json' => [
214⫶ 'schema' => [
215⫶ 'type' => 'object',
216⫶ 'properties' => [
217⫶ 'Greeting' => [
218⫶ 'type' => 'object',
219⫶ 'properties' => [
220⫶ 'salutation' => [
221⫶ 'type' => 'string',
222⫶ ],
223⫶ 'recipient' => [
224⫶ 'type' => 'string',
225⫶ ],
226⫶ 'sentence' => [
227⫶ 'type' => 'string',
228⫶ 'description' => 'Composed sentence using salutation and recipient.',
229⫶ ],
230⫶ ],
231⫶ ],
232⫶ ],
233⫶ ],
234⫶ 'example' => [
235⫶ 'Greeting' => [
236⫶ 'salutation' => 'Good day',
237⫶ 'recipient' => 'Earth',
238⫶ 'sentence' => 'Good day Earth',
239⫶ ],
240⫶ ],
241⫶ ],
242⫶ ],
243⫶ ],
244⫶ ],
245⫶ ),
246⫶)]
247⫶class DefaultController extends Controller
248⫶
249⫶//…


code_samples/api/rest_api/src/Rest/InputParser/GreetingInput.php



code_samples/api/rest_api/src/Rest/InputParser/GreetingInput.php

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@113:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@114:[[= include_file('code_samples/api/rest_api/src/Rest/InputParser/GreetingInput.php') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@115:```

code_samples/api/rest_api/src/Rest/Serializer/GreetingInputDenormalizer.php

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@111:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@112:[[= include_file('code_samples/api/rest_api/src/Rest/Serializer/GreetingInputDenormalizer.php') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@113:```

001⫶<?php declare(strict_types=1);
002⫶

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Rest\InputParser;
003⫶namespace App\Rest\Serializer;
004⫶
005⫶use App\Rest\Values\Greeting;
004⫶
005⫶use App\Rest\Values\Greeting;
006⫶use Ibexa\Contracts\Rest\Exceptions;
007⫶use Ibexa\Contracts\Rest\Input\ParsingDispatcher;
008⫶use Ibexa\Rest\Input\BaseParser;
006⫶use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
007⫶use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
008⫶use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
009⫶
009⫶
010⫶class GreetingInput extends BaseParser
010⫶class GreetingInputDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
011⫶{
011⫶{
012⫶    public function parse(array $data, ParsingDispatcher $parsingDispatcher): Greeting
013⫶ {
014⫶ if (!isset($data['Salutation'])) {
015⫶ throw new Exceptions\Parser("Missing or invalid 'Salutation' element for Greeting.");
016⫶ }
017⫶
018⫶ return new Greeting($data['Salutation'], $data['Recipient'] ?? 'World');
019⫶ }
020⫶}


code_samples/api/rest_api/src/Rest/Serializer/GreetingInputDenormalizer.php
012⫶    use DenormalizerAwareTrait;
013⫶
014⫶ public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed
015⫶ {
016⫶ if ('json' === $format) {
017⫶ $data = $data[array_key_first($data)];
018⫶ }
019⫶ $data = array_change_key_case($data);
020⫶
021⫶ $salutation = $data['salutation'] ?? 'Hello';
022⫶ $recipient = $data['recipient'] ?? 'World';
023⫶
024⫶ return new Greeting($salutation, $recipient);
025⫶ }
026⫶
027⫶ public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
028⫶ {
029⫶ if (!is_array($data)) {
030⫶ return false;
031⫶ }
032⫶
033⫶ if ('json' === $format) {
034⫶ $data = $data[array_key_first($data)];
035⫶ }
036⫶ $data = array_change_key_case($data);
037⫶
038⫶ return in_array($type, $this->getSupportedTypes($format), true) &&
039⫶ (array_key_exists('salutation', $data) || array_key_exists('recipient', $data));
040⫶ }
041⫶
042⫶ public function getSupportedTypes(?string $format): array
043⫶ {
044⫶ return [
045⫶ Greeting::class => true,
046⫶ ];
047⫶ }
048⫶}


code_samples/api/rest_api/src/Rest/Serializer/GreetingNormalizer.php



code_samples/api/rest_api/src/Rest/Serializer/GreetingNormalizer.php


code_samples/api/rest_api/src/Rest/ValueObjectVisitor/Greeting.php

docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@92:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@93:[[= include_file('code_samples/api/rest_api/src/Rest/ValueObjectVisitor/Greeting.php') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@94:```
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@101:``` php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@102:[[= include_file('code_samples/api/rest_api/src/Rest/Serializer/GreetingNormalizer.php') =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@103:```

001⫶<?php declare(strict_types=1);
002⫶

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Rest\ValueObjectVisitor;
003⫶namespace App\Rest\Serializer;
004⫶
004⫶
005⫶use Ibexa\Contracts\Rest\Output\Generator;
006⫶use Ibexa\Contracts\Rest\Output\ValueObjectVisitor;
007⫶use Ibexa\Contracts\Rest\Output\Visitor;
008⫶
009⫶class Greeting extends ValueObjectVisitor
010⫶{
011⫶ public function visit(Visitor $visitor, Generator $generator, $data)
012⫶ {
013⫶ $visitor->setHeader('Content-Type', $generator->getMediaType('Greeting'));
014⫶ $generator->startObjectElement('Greeting');
015⫶ $generator->attribute('href', $this->router->generate('app.rest.greeting'));
016⫶ $generator->valueElement('Salutation', $data->salutation);
017⫶ $generator->valueElement('Recipient', $data->recipient);
018⫶ $generator->valueElement('Sentence', "{$data->salutation} {$data->recipient}");
019⫶ $generator->endObjectElement('Greeting');
020⫶ }
021⫶}
005⫶use App\Rest\Values\Greeting;
006⫶use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
007⫶use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
008⫶use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
009⫶
010⫶class GreetingNormalizer implements NormalizerInterface, NormalizerAwareInterface
011⫶{
012⫶ use NormalizerAwareTrait;
013⫶
014⫶ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
015⫶ {
016⫶ return $data instanceof Greeting;
017⫶ }
018⫶
019⫶ /** @param \App\Rest\Values\Greeting $object */
020⫶ public function normalize(mixed $object, ?string $format = null, array $context = []): array|\ArrayObject|bool|float|int|null|string
021⫶ {
022⫶ $data = [
023⫶ 'Salutation' => $object->salutation,
024⫶ 'Recipient' => $object->recipient,
025⫶ 'Sentence' => "{$object->salutation} {$object->recipient}",
026⫶ ];
027⫶ if ('json' === $format) {
028⫶ $data = ['Greeting' => $data];
029⫶ }
030⫶
031⫶ return $this->normalizer->normalize($data, $format, $context);
032⫶ }
033⫶
034⫶ public function getSupportedTypes(?string $format): array
035⫶ {
036⫶ return [
037⫶ Greeting::class => true,
038⫶ ];
039⫶ }
040⫶}


code_samples/api/rest_api/src/Rest/ValueObjectVisitor/Greeting.php


Download colorized diff

@adriendupuis adriendupuis marked this pull request as ready for review July 22, 2025 13:50
Copy link
Contributor Author

@adriendupuis adriendupuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To myself: fix error found by redocly lint.

Comment on lines +141 to +150
'properties' => [
'salutation' => [
'type' => 'string',
'required' => false,
],
'recipient' => [
'type' => 'string',
'required' => false,
],
],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the right way to declare property's mandatoriness.

Spec example: https://swagger.io/specification/#simple-model

[1] openapi.yaml:206:29 at #/paths/~1greet/post/requestBody/content/application~1vnd.ibexa.api.GreetingInput+xml/schema/properties/salutation/required

Expected type `array` but got `boolean`.

204 | salutation:
205 |   type: string
206 |   required: false
    |             ^^^^^
207 | recipient:
208 |   type: string

Error was generated by the struct rule.


[2] openapi.yaml:209:29 at #/paths/~1greet/post/requestBody/content/application~1vnd.ibexa.api.GreetingInput+xml/schema/properties/recipient/required

Expected type `array` but got `boolean`.

207 |     recipient:
208 |       type: string
209 |       required: false
    |                 ^^^^^
210 | example:
211 |   salutation: 'Good morning'

Error was generated by the struct rule.
Suggested change
'properties' => [
'salutation' => [
'type' => 'string',
'required' => false,
],
'recipient' => [
'type' => 'string',
'required' => false,
],
],
'required' => [],
'properties' => [
'salutation' => [
'type' => 'string',
],
'recipient' => [
'type' => 'string',
],
],

Comment on lines +162 to +171
'properties' => [
'salutation' => [
'type' => 'string',
'required' => false,
],
'recipient' => [
'type' => 'string',
'required' => false,
],
],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[3] openapi.yaml:218:71 at #/paths/~1greet/post/requestBody/content/application~1vnd.ibexa.api.GreetingInput+json/schema/properties/GreetingInput/properties/salutation/required

Expected type `array` but got `boolean`.

216 |     GreetingInput:
217 |       type: object
218 |       properties: { salutation: { type: string, required: false }, recipient: { type: string, required: false } }
    |                                                           ^^^^^
219 | example:
220 |   GreetingInput:

Error was generated by the struct rule.


[4] openapi.yaml:218:117 at #/paths/~1greet/post/requestBody/content/application~1vnd.ibexa.api.GreetingInput+json/schema/properties/GreetingInput/properties/recipient/required

Expected type `array` but got `boolean`.

216 |     GreetingInput:
217 |       type: object
218 |       properties: { salutation: { type: string, required: false }, recipient: { type: string, required: false } }
    |                                                                                                         ^^^^^
219 | example:
220 |   GreetingInput:

Error was generated by the struct rule.
Suggested change
'properties' => [
'salutation' => [
'type' => 'string',
'required' => false,
],
'recipient' => [
'type' => 'string',
'required' => false,
],
],
'required' => [],
'properties' => [
'salutation' => [
'type' => 'string',
],
'recipient' => [
'type' => 'string',
],
],

Copy link
Contributor Author

@adriendupuis adriendupuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +25 to +36
parameters: [
new Model\Parameter(
name: 'Accept',
in: 'header',
required: false,
description: 'If set, the greeting is returned in XML or JSON format.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.Greeting+json',
),
],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parameters: [
new Model\Parameter(
name: 'Accept',
in: 'header',
required: false,
description: 'If set, the greeting is returned in XML or JSON format.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.Greeting+json',
),
],
parameters: [],

Comment on lines +109 to +130
parameters: [
new Model\Parameter(
name: 'Content-Type',
in: 'header',
required: false,
description: 'The greeting input schema encoded in XML or JSON.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.GreetingInput+json',
),
new Model\Parameter(
name: 'Accept',
in: 'header',
required: false,
description: 'If set, the greeting is returned in XML or JSON format.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.Greeting+json',
),
],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parameters: [
new Model\Parameter(
name: 'Content-Type',
in: 'header',
required: false,
description: 'The greeting input schema encoded in XML or JSON.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.GreetingInput+json',
),
new Model\Parameter(
name: 'Accept',
in: 'header',
required: false,
description: 'If set, the greeting is returned in XML or JSON format.',
schema: [
'type' => 'string',
],
example: 'application/vnd.ibexa.api.Greeting+json',
),
],
parameters: [],

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant