I am writing end2end tests for a rest api in a Symfony 4 project.
I use php 7.4, Swagger annotation, nelmio/api-doc-bundle 3.6.1, nelmio/cors-bundle 1.5.6
This is my controller code for the method.
/**
* @Route(path="", name="create", methods={"POST"})
* @ValidationGroups({"create"})
*
* @SWG\Post(
* path="/api/professions",
* tags={"Endpoint: Professions"},
* summary="save a profession",
* @SWG\Parameter(
* name="ProfessionDto",
* required=true,
* in="body",
* @Model(type=ProfessionDto::class)
* ),
* @SWG\Response(
* response=201,
* description="created",
* )
* )
*/
public function add(ProfessionDto $professionDto): CreatedView
{
$professionDto = $this->professionService->insert($professionDto);
return $this->created(['profession' => $professionDto]);
}
ProfessionDto is the object defining the exchanged data. It contains as property some more objects, since I want to have some structure in the returned data and not just a heap of key value pairs.
In the class ProfessionDto I defined the properties, which relate to other objects e.g.:
namespace App\Api\Dto;
use Nelmio\ApiDocBundle\Annotation\Model;
use Swagger\Annotations as SWG;
use Symfony\Component\Serializer\Annotation\Groups as SerializerGroups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @SWG\Definition()
*/
class ProfessionDto
{
use HistoryDto;
/**
* @SWG\Property(description="Id", type="integer")
*
* @SerializerGroups({"view", "update", "collection"})
*/
public ?int $id = null;
/**
* @Assert\NotBlank(groups={"create"})
* @Assert\Type("string")
* @SWG\Property(description="...", type="string")
*/
public string $name;
/**
* @Assert\Type("boolean")
* @SWG\Property(description="ist aktiviert/deaktiviert", type="boolean")
*/
public bool $active;
/**
* @SWG\Property(
* description="...",
* type="object",
* ref=@Model(type=ProfessionAreaDto::class)
* )
*
* @SerializerGroups({"view", "create", "update"})
*/
public ?ProfessionAreaDto $professionArea = null;
/**
* @SWG\Property(
* description="...",
* type="object",
* ref=@Model(type=ProfessionalActivityDto::class)
* )
*
* @SerializerGroups({"view", "create", "update"})
*/
public ?ProfessionalActivityDto $professionalActivity = null;
/**
* @SWG\Property(
* description="...",
* type="object",
* ref=@Model(type=IntroductionDto::class)
* )
*
* @SerializerGroups({"view", "create", "update"})
*/
public ?IntroductionDto $introduction = null;
/**
* @SWG\Property(
* description="...",
* type="object",
* ref=@Model(type=PerformanceBehaviourDto::class)
* )
*
* @SerializerGroups({"view", "create", "update"})
*/
public ?PerformanceBehaviourDto $performanceBehaviour = null;
}
If I call the api with postman or via my test and pass in my data as json e.g.
{
"id":null,
"name":"fancy new data",
"active":true,
"professionArea":{
"id":48,
"name":"Vitae nulla aperiam aut enim.",
"active":true,
"signature":null,
"signatureTypeId":null,
"transition":null,
"infotextCheck":null
},
"professionalActivity":{
"id":null,
"textMResignationTestimonial":null,
...
"changedBy":null
},
"introduction":{
"id":null,
"textMResignationTestimonial":null,
"textMInterimTestimonial":null,
...
"changedOn":null,
"changedBy":null
},
"performanceBehaviour":{
"id":null,
"textMResignationGrade1":null,
"textMInterimGrade1":null,
...
"changedOn":null,
"changedBy":null
},
"createdOn":null,
"createdBy":null,
"changedOn":null,
"changedBy":null
}
I get the error message: TypeError: Typed property App\Api\Dto\ProfessionDto::$professionArea must be an instance of App\Api\Dto\ProfessionAreaDto or null, array used
What did I do wrong? Do I expect too much? are objects within objects not possible?
Since this project didn't use the friendsofsymfony/rest-bundle and the Controller didn't extend the AbstractFOSRestController.
So the solution to my problem was to manually write a Denormalizer class, which maps my array back to the proper objects.
This is how I did it:
namespace App\Serializer;
use App\Api\Dto\IntroductionDto;
use App\Api\Dto\PerformanceBehaviourDto;
use App\Api\Dto\ProfessionalActivityDto;
use App\Api\Dto\ProfessionAreaDto;
use App\Api\Dto\ProfessionDto;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class ProfessionDeNormalizer implements DenormalizerInterface
{
public function denormalize($data, $type, $format = null, array $context = [])
{
$dto = new ProfessionDto();
$areaDto = new ProfessionAreaDto();
$introDto = new IntroductionDto();
$activityDto = new ProfessionalActivityDto();
$performanceDto = new PerformanceBehaviourDto();
foreach($data['professionArea'] as $key => $value) {
$areaDto->$key = $value;
}
unset($data['professionArea']);
foreach($data['introduction'] as $key => $value) {
$introDto->$key = $value;
}
unset($data['introduction']);
foreach($data['professionalActivity'] as $key => $value) {
$areaDto->$key = $value;
}
unset($data['professionalActivity']);
foreach($data['performanceBehaviour'] as $key => $value) {
$areaDto->$key = $value;
}
unset($data['performanceBehaviour']);
foreach($data as $key => $value) {
$dto->$key = $value;
}
$dto->professionArea = $areaDto;
$dto->professionalActivity = $activityDto;
$dto->performanceBehaviour = $performanceDto;
$dto->introduction = $introDto;
return $dto;
}
public function supportsDenormalization($data, $type, $format = null)
{
return ProfessionDto::class === $type;
}
}