Skip to content

Make resource constructor parameters writables #1749

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

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
31 changes: 31 additions & 0 deletions features/serializer/deserialize_objects_using_constructor.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Feature: Resource with constructor deserializable
In order to build non anemic resource object
As a developer
I should be able to deserialize data into objects with constructors

@createSchema
Scenario: post a resource built with constructor
When I add "Content-Type" header equal to "application/ld+json"
And I send a "POST" request to "/dummy_entity_with_constructors" with body:
"""
{
"foo": "hello",
"bar": "world"
}
"""
Then the response status code should be 201
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON should be equal to:
"""
{
"@context": "/contexts/DummyEntityWithConstructor",
"@id": "/dummy_entity_with_constructors/1",
"@type": "DummyEntityWithConstructor",
"id": 1,
"foo": "hello",
"bar": "world",
"baz": null
}
"""

Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ public function create(string $resourceClass, string $name, array $options = [])
$propertyMetadata = $propertyMetadata->withWritable($writable);
}

if (method_exists($this->propertyInfo, 'isInitializable')) {
if (null === $propertyMetadata->isInitializable() && null !== $initializable = $this->propertyInfo->isInitializable($resourceClass, $name, $options)) {
$propertyMetadata = $propertyMetadata->withInitializable($initializable);
}
} else {
// BC layer for Symfony < 4.2
$ref = new \ReflectionClass($resourceClass);
if ($ref->isInstantiable() && $constructor = $ref->getConstructor()) {
foreach ($constructor->getParameters() as $constructorParameter) {
if ($constructorParameter->name === $name && null === $propertyMetadata->isInitializable()) {
$propertyMetadata = $propertyMetadata->withInitializable(true);
}
}
}
}

return $propertyMetadata;
}
}
2 changes: 1 addition & 1 deletion src/Hydra/Serializer/DocumentationNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ private function getProperty(PropertyMetadata $propertyMetadata, string $propert
'hydra:title' => $propertyName,
'hydra:required' => $propertyMetadata->isRequired(),
'hydra:readable' => $propertyMetadata->isReadable(),
'hydra:writable' => $propertyMetadata->isWritable(),
'hydra:writable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
];

if (null !== $range = $this->getRange($propertyMetadata)) {
Expand Down
29 changes: 28 additions & 1 deletion src/Metadata/Property/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ final class PropertyMetadata
private $childInherited;
private $attributes;
private $subresource;
private $initializable;

public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null)
public function __construct(Type $type = null, string $description = null, bool $readable = null, bool $writable = null, bool $readableLink = null, bool $writableLink = null, bool $required = null, bool $identifier = null, string $iri = null, $childInherited = null, array $attributes = null, SubresourceMetadata $subresource = null, bool $initializable = null)
{
$this->type = $type;
$this->description = $description;
Expand All @@ -49,6 +50,7 @@ public function __construct(Type $type = null, string $description = null, bool
$this->childInherited = $childInherited;
$this->attributes = $attributes;
$this->subresource = $subresource;
$this->initializable = $initializable;
}

/**
Expand Down Expand Up @@ -381,4 +383,29 @@ public function withSubresource(SubresourceMetadata $subresource = null): self

return $metadata;
}

/**
* Is initializable?
*
* @return bool|null
*/
public function isInitializable()
{
return $this->initializable;
}

/**
* Returns a new instance with the given initializable flag.
*
* @param bool $initializable
*
* @return self
*/
public function withInitializable(bool $initializable): self
{
$metadata = clone $this;
$metadata->initializable = $initializable;

return $metadata;
}
}
6 changes: 4 additions & 2 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu

if (
Copy link
Contributor

Choose a reason for hiding this comment

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

if (
    $this->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
    (
        isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
        isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !is_object($classOrObject) && $propertyMetadata->isInitializable())
    )
) {
    $allowedAttributes[] = $propertyName;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Long lines should be avoided, isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Split across more lines != easier to read 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

I think many nested levels of brackets, especially with uneven indentation levels, is much worse than long lines.

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->isAllowedAttribute($classOrObject, $propertyName, null, $context) &&
((isset($context['api_normalize']) && $propertyMetadata->isReadable()) ||
(isset($context['api_denormalize']) && $propertyMetadata->isWritable()))
(
isset($context['api_normalize']) && $propertyMetadata->isReadable() ||
isset($context['api_denormalize']) && ($propertyMetadata->isWritable() || !is_object($classOrObject) && $propertyMetadata->isInitializable())
)
) {
$allowedAttributes[] = $propertyName;
}
Expand Down
29 changes: 29 additions & 0 deletions tests/Fixtures/DummyObjectWithConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Fixtures;

/**
* @author Maxime Veber <[email protected]>
*/
class DummyObjectWithConstructor
{
private $foo;
private $bar;

public function __construct(string $foo, \stdClass $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}
}
32 changes: 32 additions & 0 deletions tests/Fixtures/DummyObjectWithoutConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Fixtures;

/**
* @author Maxime Veber <[email protected]>
*/
class DummyObjectWithoutConstructor
{
private $foo;

public function getFoo()
{
return $this->foo;
}

public function setFoo($foo)
{
$this->foo = $foo;
}
}
112 changes: 112 additions & 0 deletions tests/Fixtures/TestBundle/Entity/DummyEntityWithConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
* Dummy entity built with constructor.
* https://github.com/api-platform/core/issues/1747.
*
* @author Maxime Veber <[email protected]>
*
* @ApiResource(
* itemOperations={
* "get",
* "put"={"denormalization_context"={"groups"={"put"}}}
* }
* )
* @ORM\Entity
*/
class DummyEntityWithConstructor
{
/**
* @var int The id
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @var string
*
* @ORM\Column
*/
private $foo;

/**
* @var string
*
* @ORM\Column
*/
private $bar;

/**
* @var string
*
* @ORM\Column(nullable=true)
* @Groups({"put"})
*/
private $baz;

public function __construct(string $foo, string $bar)
{
$this->foo = $foo;
$this->bar = $bar;
}

/**
* @return int
*/
public function getId(): int
{
return $this->id;
}

/**
* @return string
*/
public function getFoo(): string
{
return $this->foo;
}

/**
* @return string
*/
public function getBar(): string
{
return $this->bar;
}

/**
* @return string
*/
public function getBaz()
{
return $this->baz;
}

/**
* @param string $baz
*/
public function setBaz(string $baz)
{
$this->baz = $baz;
}
}
4 changes: 4 additions & 0 deletions tests/Metadata/Property/PropertyMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public function testValueObject()
$this->assertNotSame($metadata, $newMetadata);
$this->assertEquals(['a' => 'b'], $newMetadata->getAttributes());
$this->assertEquals('b', $newMetadata->getAttribute('a'));

$newMetadata = $metadata->withInitializable(true);
$this->assertNotSame($metadata, $newMetadata);
$this->assertTrue($newMetadata->isInitializable());
}

public function testShouldReturnRequiredFalseWhenRequiredTrueIsSetButMaskedByWritableFalse()
Expand Down