Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Classes/Serializer/Subscriber/AbstractEntitySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function onPostSerialize(ObjectEvent $event): void
$visitor = $event->getVisitor();

$this->addForceEntityProperties($entity, $visitor);
$this->addIri($entity, $visitor);
$this->addIri($entity, $visitor, $event->getType()['name'] ?? '');
}

public function onPreDeserialize(PreDeserializeEvent $event): void
Expand Down Expand Up @@ -79,9 +79,22 @@ protected function addForceEntityProperties(AbstractDomainObject $entity, JsonSe
}
}

protected function addIri(AbstractDomainObject $entity, JsonSerializationVisitor $visitor): void
/**
* @param string $declaredType Type of the serialized value declared in serializer metadata (e.g. `type` in YAML)
*/
protected function addIri(AbstractDomainObject $entity, JsonSerializationVisitor $visitor, string $declaredType = ''): void
{
$apiResource = $this->apiResourceRepository->getByEntity($entity);

// Related object is not an API resource, but YAML metadata declares API resource class as its `type`
if (
!$apiResource instanceof ApiResource
&& $declaredType !== ''
&& is_subclass_of($declaredType, get_class($entity))
) {
$apiResource = $this->apiResourceRepository->getByEntity($declaredType);
}

if ($apiResource instanceof ApiResource && $apiResource->getMainItemOperation() instanceof ItemOperation) {
// @todo should be generated with symfony router
$iri = str_replace(
Expand Down
9 changes: 9 additions & 0 deletions Documentation/Integration/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ Models of other extensions (e.g. ``EXT:news``) can be exposed in the API without
#. Map your model to the table of the original model with Extbase persistence configuration (``Configuration/Extbase/Persistence/Classes.php``). If your class should also be returned by the original repositories and relations, register it as ``subclasses`` / ``recordType`` as described in TYPO3 documentation.
#. Configure serialization of properties of the original class with :ref:`YAML metadata <serialization_yaml-metadata>` - annotations can not be added to classes you do not own.

Extbase creates related objects with the classes used in the original model (e.g. categories of a news are ``\GeorgRinger\News\Domain\Model\Category``). Declare the type of such relation with your API resource class in YAML metadata - related objects are then serialized as that API resource, including its ``@id``:

.. code-block:: yaml

GeorgRinger\News\Domain\Model\News:
properties:
categories:
type: 'TYPO3\CMS\Extbase\Persistence\ObjectStorage<SourceBroker\T3apinews\Domain\Model\Category>'

News extension - Example integration
======================================

Expand Down
143 changes: 77 additions & 66 deletions Documentation/Operations/Index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,14 @@ Here is an example of basic response for collection operation (shortened to firs
{
"title": "[EN] Category 1A",
"image": null,
"uid": 1
"uid": 1,
"@id": "/_api/news/categories/1"
},
{
"title": "[EN] Category 2A",
"image": null,
"uid": 2
"uid": 2,
"@id": "/_api/news/categories/2"
}
],
"type": "0",
Expand All @@ -157,17 +159,19 @@ Here is an example of basic response for collection operation (shortened to firs
"tags": [
{
"title": "[EN] Tag 1A",
"uid": 1
"uid": 1,
"@id": "/_api/news/tags/1"
},
{
"title": "[EN] Tag 4A",
"uid": 4
"uid": 4,
"@id": "/_api/news/tags/4"
}
],
"pathSegment": "en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"singleUri": "https://14.t3api.ddev.site/news/en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_6e0c6b2eb1.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_fe74a17411.jpg",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_cb256deee9.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_f67e7c21bd.jpg",
"uid": 1,
"@id": "/_api/news/news/1"
},
Expand All @@ -182,7 +186,8 @@ Here is an example of basic response for collection operation (shortened to firs
{
"title": "[EN] Category 2A",
"image": null,
"uid": 2
"uid": 2,
"@id": "/_api/news/categories/2"
}
],
"type": "0",
Expand Down Expand Up @@ -214,17 +219,19 @@ Here is an example of basic response for collection operation (shortened to firs
"tags": [
{
"title": "[EN] Tag 1A",
"uid": 1
"uid": 1,
"@id": "/_api/news/tags/1"
},
{
"title": "[EN] Tag 2A",
"uid": 2
"uid": 2,
"@id": "/_api/news/tags/2"
}
],
"pathSegment": "en-natus-error-sit-voluptatem-folder-a",
"singleUri": "https://14.t3api.ddev.site/news/en-natus-error-sit-voluptatem-folder-a",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_6e0c6b2eb1.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_fe74a17411.jpg",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_cb256deee9.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_f67e7c21bd.jpg",
"uid": 2,
"@id": "/_api/news/news/2"
}
Expand Down Expand Up @@ -286,61 +293,65 @@ because they are useless in single item operation context. Example response for
.. code-block:: json

{
"title": "[EN] Sed ut perspiciatis unde omnis iste natus error sit voluptatem folder A",
"alternativeTitle": "",
"teaser": "",
"bodytext": "<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>",
"datetime": "2020-05-28T21:20:00.000+02:00",
"author": "",
"authorEmail": "",
"categories": [
{
"title": "[EN] Category 1A",
"description": "",
"image": null,
"uid": 1
},
{
"title": "[EN] Category 2A",
"description": "",
"image": null,
"uid": 2
}
],
"related": [],
"relatedFrom": [],
"type": "0",
"falMedia": [
{
"url": "https://14.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
"uid": 4,
"file": {
"uid": 1,
"name": "test1.jpg",
"mimeType": "image/jpeg",
"size": 42520
}
}
],
"internalurl": "",
"externalurl": "",
"istopnews": false,
"tags": [
{
"title": "[EN] Tag 1A",
"uid": 1
},
{
"title": "[EN] Tag 4A",
"uid": 4
}
],
"pathSegment": "en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"singleUri": "https://14.t3api.ddev.site/news/en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_6e0c6b2eb1.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_fe74a17411.jpg",
"uid": 1,
"@id": "/_api/news/news/1"
"title": "[EN] Sed ut perspiciatis unde omnis iste natus error sit voluptatem folder A",
"alternativeTitle": "",
"teaser": "",
"bodytext": "<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>",
"datetime": "2020-05-28T21:20:00.000+02:00",
"author": "",
"authorEmail": "",
"categories": [
{
"title": "[EN] Category 1A",
"description": "",
"image": null,
"uid": 1,
"@id": "/_api/news/categories/1"
},
{
"title": "[EN] Category 2A",
"description": "",
"image": null,
"uid": 2,
"@id": "/_api/news/categories/2"
}
],
"related": [],
"relatedFrom": [],
"type": "0",
"falMedia": [
{
"url": "https://14.t3api.ddev.site/fileadmin/user_upload/test1.jpg",
"uid": 4,
"file": {
"uid": 1,
"name": "test1.jpg",
"mimeType": "image/jpeg",
"size": 42520
}
}
],
"internalurl": "",
"externalurl": "",
"istopnews": false,
"tags": [
{
"title": "[EN] Tag 1A",
"uid": 1,
"@id": "/_api/news/tags/1"
},
{
"title": "[EN] Tag 4A",
"uid": 4,
"@id": "/_api/news/tags/4"
}
],
"pathSegment": "en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"singleUri": "https://14.t3api.ddev.site/news/en-sed-ut-perspiciatis-unde-omnis-iste-natus-error-sit-voluptatem-folder-a",
"imageThumbnail": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_cb256deee9.jpg",
"imageLarge": "https://14.t3api.ddev.site/fileadmin/_processed_/a/7/csm_test1_f67e7c21bd.jpg",
"uid": 1,
"@id": "/_api/news/news/1"
}

Customizing returned properties
Expand Down
59 changes: 59 additions & 0 deletions Tests/Functional/Dispatcher/RelatedResourceIriDispatcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace SourceBroker\T3api\Tests\Functional\Dispatcher;

use PHPUnit\Framework\Attributes\Test;

/**
* API resources often extend models of other extensions (e.g. EXT:news). Extbase creates related
* objects with the class from the property type of the extended model, so they are not instances
* of the API resource class. When serializer metadata declares the property with the API resource
* class, related objects are serialized with `@id` of that API resource.
*
* Fixture: `Product` relations `category` (`Category`) and `tags` (`ObjectStorage<Tag>`) are declared
* as `CategoryResource` / `ObjectStorage<TagResource>` in serializer YAML metadata of the fixture extension.
*/
class RelatedResourceIriDispatcherTest extends AbstractDispatcherTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->importCSVDataSet(__DIR__ . '/../Fixtures/products.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/categories.csv');
$this->importCSVDataSet(__DIR__ . '/../Fixtures/tags.csv');
}

#[Test]
public function relatedObjectHasIriOfApiResourceDeclaredInSerializerMetadata(): void
{
$product = $this->dispatchProductGet(1);

self::assertSame('/_api/categories/1', $product['category']['@id'] ?? null);
}

#[Test]
public function relatedObjectsInObjectStorageHaveIriOfApiResourceDeclaredInSerializerMetadata(): void
{
$product = $this->dispatchProductGet(1);

self::assertSame(['/_api/tags/1', '/_api/tags/2'], array_column($product['tags'], '@id'));
}

#[Test]
public function apiResourceItselfKeepsItsIri(): void
{
self::assertSame('/_api/products/1', $this->dispatchProductGet(1)['@id']);
}

private function dispatchProductGet(int $uid): array
{
return json_decode(
$this->dispatchGet('https://example.com/_api/products/' . $uid),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace T3apiTests\FunctionalTest\Domain\Model;

use SourceBroker\T3api\Annotation\ApiResource;

/**
* API resource extending a model which is not an API resource - the same way as API resources
* extend models of 3rd party extensions. `Product` relations are declared with this class
* only in serializer YAML metadata (see Resources/Private/Serializer), while Extbase still
* creates `Category` objects.
*
* @ApiResource(
* itemOperations={
* "get": {
* "path": "/categories/{id}"
* }
* }
* )
*/
class CategoryResource extends Category {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace T3apiTests\FunctionalTest\Domain\Model;

use SourceBroker\T3api\Annotation\ApiResource;

/**
* API resource extending a model which is not an API resource - the same way as API resources
* extend models of 3rd party extensions. `Product` relations are declared with this class
* only in serializer YAML metadata (see Resources/Private/Serializer), while Extbase still
* creates `Tag` objects.
*
* @ApiResource(
* itemOperations={
* "get": {
* "path": "/tags/{id}"
* }
* }
* )
*/
class TagResource extends Tag {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use T3apiTests\FunctionalTest\Domain\Model\CategoryResource;
use T3apiTests\FunctionalTest\Domain\Model\TagResource;

return [
CategoryResource::class => [
'tableName' => 'tx_functionaltest_domain_model_category',
],
TagResource::class => [
'tableName' => 'tx_functionaltest_domain_model_tag',
],
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
T3apiTests\FunctionalTest\Domain\Model\Product:
properties:
category:
type: 'T3apiTests\FunctionalTest\Domain\Model\CategoryResource'
tags:
type: 'TYPO3\CMS\Extbase\Persistence\ObjectStorage<T3apiTests\FunctionalTest\Domain\Model\TagResource>'
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

defined('TYPO3') || die('Access denied.');

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializerMetadataDirs'] = array_merge(
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['t3api']['serializerMetadataDirs'] ?? [],
[
't3api_functional_test' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('t3api_functional_test')
. 'Resources/Private/Serializer',
]
);
Loading
Loading