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

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
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