Skip to content

IBX-8190: Update REST new resource #2682

New issue

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

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

Already on GitHub? # to your account

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft

IBX-8190: Update REST new resource #2682

wants to merge 22 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:

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."
Copy link

github-actions bot commented Apr 2, 2025

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@167:```php
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@168:[[= include_file('code_samples/api/rest_api/src/Rest/Controller/DefaultController.php', 0, 246) =]]
docs/api/rest_api/extending_rest_api/creating_new_rest_resource.md@169:```

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⫶)]


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)
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 = [])
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): 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 Greeting::class === $type &&
039⫶ (array_key_exists('salutation', $data) || array_key_exists('recipient', $data));
040⫶ }
041⫶}


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)
015⫶ {
016⫶ return $data instanceof Greeting;
017⫶ }
018⫶
019⫶ /** @param Greeting $object */
020⫶ public function normalize(mixed $object, ?string $format = null, array $context = []): mixed
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⫶}


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


Download colorized diff

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)

if ('json' === $format) {
$data = $data[array_key_first($data)];
}
$data = array_change_key_case($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.

Make node name case insensitive. Could be removed for more strictness.

@adriendupuis adriendupuis changed the title IBX-8190: Update REST customization IBX-8190: Update REST new resource Apr 2, 2025
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant