Skip to content
Open
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,25 @@
## requirements

You need PHP (7.x), composer, and Symfony 4
Even if i didn't try without ApiPlatform, it should work without this component. We only use ValidationException from ApiPlatform Sf bridge because ApiPlatform rely on it to return HTTP 4x error.
If your Api doesn't rely on this component, you will just have to add a listener on this kind of exception to manage the Response.

## explanation

Working with ApiPlatform, i wanted to use custom POST route where i could send complex json data which represents nested entities.
To realize this i choose to use the ParamConverters. So with little convention (json props must be the same as php entity props)
and few ParamConverters (one per entity) extending the Rebolon/Request/ItemAbstractConverter (for one entity) or ListAbstractConverter (for collection of entities), it works !

For instance it works finely in **Creation** mode.
Also, when you send sub-entities with all properties even an ID, then, the component consider that you want to use the existing entity with the specified ID.
It will then ignore all other fields. This is a security to prevent update on sub-entities. But maybe this feature is missing and in this case, open an issue !

When you do a PUT HTTP, only the rot entity will be updated. If you have nested entities with associative entities, it's up to you to manage the wished behavior in the Controller.
The ParamConverter will not take any decision so:
* if you already have entries in this kind of relations, they will be kept, and those you specified in the JSON will be added
* if you want to replace those relations with the new ones, you will have to delete them (all relations that have ID are the old ones) in the Controller before persist the Book entity
* if you want to update those relations

Here are some samples of json sent to the custom routes:

```
Expand Down
63 changes: 54 additions & 9 deletions src/Request/ParamConverter/AbstractConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
*/
abstract class AbstractConverter implements ConverterInterface
{
const HTTP_PUT = 'PUT';
const HTTP_POST = 'POST';

/**
* @var
*/
protected $method;

/**
* @var array
*/
Expand Down Expand Up @@ -84,6 +92,9 @@ public function supports(ParamConverter $configuration)
*/
public function apply(Request $request, ParamConverter $configuration)
{
// will allow to manage Update vs Insert
$this->method = $request->getMethod();

$content = $request->getContent();

if (!$content) {
Expand All @@ -101,8 +112,6 @@ public function apply(Request $request, ParamConverter $configuration)
sprintf('Wrong parameter to create new %s (generic)', static::RELATED_ENTITY),
420
);

return false;
}

if ($raw
Expand All @@ -113,6 +122,38 @@ public function apply(Request $request, ParamConverter $configuration)
return true;
}

/**
* Not exposed in ConverterInterface, it exists for testing purpose
*
* @return $this
*/
final public function setInsertMode()
{
if ($this->method && $this->method !== self::HTTP_POST) {
throw new \RuntimeException('Http method can not be changed');
}

$this->method = self::HTTP_POST;

return $this;
}

/**
* Not exposed in ConverterInterface, it exists for testing purpose
*
* @return $this
*/
final public function setUpdateMode()
{
if ($this->method && $this->method !== self::HTTP_PUT) {
throw new \RuntimeException('Http method can not be changed');
}

$this->method = self::HTTP_PUT;

return $this;
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -174,10 +215,12 @@ public function initFromRequest($jsonOrArray, $propertyPath)
* @throws RuntimeException
* @throws \TypeError
*/
protected function buildEntity($json)
protected function buildEntity($json, $entity = null)
{
$className = static::RELATED_ENTITY;
$entity = new $className();
if (!$entity) {
$className = static::RELATED_ENTITY;
$entity = new $className();
}

$this->buildWithEzProps($json, $entity);
$this->buildWithManyRelProps($json, $entity);
Expand Down Expand Up @@ -378,10 +421,12 @@ protected function useRegistry($relation, $operationsInfo)
*/
protected function getFromDatabase($id, $class = null)
{
if (!$class && static::RELATED_ENTITY) {
$class = static::RELATED_ENTITY;
} else {
throw new \InvalidArgumentException(sprintf('You must define constant RELATED_ENTITY form you ParamConverter %s', static::name));
if (!$class) {
if(static::RELATED_ENTITY) {
$class = static::RELATED_ENTITY;
} else {
throw new \InvalidArgumentException(sprintf('You must define constant RELATED_ENTITY form you ParamConverter %s', static::name));
}
}

$entityExists = $this->entityManager
Expand Down
33 changes: 23 additions & 10 deletions src/Request/ParamConverter/ItemAbstractConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
*/
abstract class ItemAbstractConverter extends AbstractConverter
{
/**
* only root element can be updated, all nested entities won't be even if they have ID. All other fields of thoses entities will be forgiven
*/
const ROOT_LIMIT_FOR_UPDATE = 1;

/**
* @inheritdoc
*/
Expand All @@ -24,23 +29,31 @@ public function initFromRequest($jsonOrArray, $propertyPath)
$json = $this->checkJsonOrArray($jsonOrArray);

$idPropertyIsInJson = false;
$entity = null;
if (!is_array($json)
|| ($idPropertyIsInJson = array_key_exists($this->getIdProperty(), $json))
) {
/**
* We don't care of other properties. We don't accept update on sub-entity, we can create or re-use
* So here we just clean json and replace it with the id content
*/
if ($idPropertyIsInJson) {
$json = $json[$this->getIdProperty()];
}

array_pop(self::$propertyPath);
if (count(self::$propertyPath) > self::ROOT_LIMIT_FOR_UPDATE) {
/**
* We don't care of other properties. We don't accept update on sub-entity, we can create or re-use
* So here we just clean json and replace it with the id content
*/
if ($idPropertyIsInJson) {
$json = $json[$this->getIdProperty()];
}

return $this->getFromDatabase($json);
array_pop(self::$propertyPath);

return $this->getFromDatabase($json);
}

if ($idPropertyIsInJson) {
$entity = $this->getFromDatabase($json[$this->getIdProperty()], static::RELATED_ENTITY);
}
}

$entity = $this->buildEntity($json);
$entity = $this->buildEntity($json, $entity);

array_pop(self::$propertyPath);

Expand Down
34 changes: 33 additions & 1 deletion src/Request/ParamConverter/ListAbstractConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
*/
abstract class ListAbstractConverter extends AbstractConverter
{
/**
* list of entities that represents associative tables can be updated only if they are just after the main root element
* i can identify it with the property path: book > editors > [x] which means rootEntity > associativeEntities > EntityInList
* so it's the third item in propertyPath
*/
const ROOT_LIMIT_FOR_UPDATE = 3;

/**
* @inheritdoc
*/
Expand All @@ -33,7 +40,32 @@ public function initFromRequest($jsonOrArray, $propertyPath)
foreach ($listItems as $item) {
self::$propertyPath[count(self::$propertyPath)] = '[' . count($entities) . ']';

$entities[] = $this->buildEntity($item);
$idPropertyIsInJson = false;
$entity = null;
if (!is_array($item)
|| $idPropertyIsInJson = array_key_exists($this->getIdProperty(), $item)
) {
if (count(self::$propertyPath) > self::ROOT_LIMIT_FOR_UPDATE) {
/**
* We don't care of other properties. We don't accept update on sub-entity, we can create or re-use
* So here we just clean json and replace it with the id content
*/
if ($idPropertyIsInJson) {
$item = $item[$this->getIdProperty()];
}

array_pop(self::$propertyPath);

$entity = $this->getFromDatabase($item);
}

if (!$entity
&& $idPropertyIsInJson) {
$entity = $this->getFromDatabase($item[$this->getIdProperty()], static::RELATED_ENTITY);
}
}

$entities[] = $this->buildEntity($item, $entity);

array_pop(self::$propertyPath);
}
Expand Down
4 changes: 4 additions & 0 deletions tests/ApiJsonParamConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public function testWithAllEntitiesToBeCreatedExcept2AuthorsInsteadOf3()
$entityManager = $this->createMock(EntityManagerInterface::class);
$bookConverter = $this->getBookConverter($entityManager);

$bookConverter->setInsertMode();
$book = $bookConverter->initFromRequest(json_encode($content->book), 'book');

$this->assertEquals($content->book->title, $book->getTitle());
Expand Down Expand Up @@ -141,6 +142,7 @@ public function testWithExistingEntity()

$bookConverter = $this->getBookConverter($entityManager);

$bookConverter->setInsertMode();
$book = $bookConverter->initFromRequest(json_encode($content->book), 'book');

$this->assertEquals($content->book->title, $book->getTitle());
Expand Down Expand Up @@ -169,6 +171,7 @@ public function testWithExistingEntityButWithFullProps()

$bookConverter = $this->getBookConverter($entityManager);

$bookConverter->setInsertMode();
$book = $bookConverter->initFromRequest(json_encode($content->book), 'book');

$this->assertEquals($content->book->title, $book->getTitle());
Expand All @@ -188,6 +191,7 @@ public function testWithWrongJson()

$bookConverter = $this->getBookConverter($entityManager);

$bookConverter->setInsertMode();
$bookConverter->initFromRequest(json_encode($content->book), 'book');
}

Expand Down