Trying to mock a doctrine repository inside a test, the returnValueMap() is always returning NULL when used with the findOneBy method.
I have mocked two entities then tried to mock their repository with a given return value map. The test fails and debugging shows that the returnValueMap() is returning NULL.
Here is the class to be tested (the denormalizer)
<?php
declare(strict_types=1);
namespace App\Serializer;
use App\Entity\AdditionalService;
use App\Repository\AdditionalServiceRepository;
use Dto\AdditionalServiceCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class AdditionalServiceCollectionDenormalizer implements DenormalizerInterface
{
/** #var AdditionalServiceRepository */
private $additionalServiceRepository;
public function __construct(AdditionalServiceRepository $additionalServiceRepository)
{
$this->additionalServiceRepository = $additionalServiceRepository;
}
public function denormalize($mappedCsvRow, $class, $format = null, array $context = [])
{
$addtionalServicesCollection = new AdditionalServiceCollection();
foreach ($mappedCsvRow as $fieldName => $fieldValue) {
/** #var AdditionalService $additionalService */
$additionalService = $this->additionalServiceRepository->findOneBy(['name'=>$fieldName]);
if ($additionalService) {
$addtionalServicesCollection->add($additionalService->getId(), $fieldValue);
}
}
return $addtionalServicesCollection;
}
public function supportsDenormalization($data, $type, $format = null)
{
return $type instanceof AdditionalServiceCollection;
}
}
Here is my test class:
<?php
namespace App\Tests\Import\Config;
use App\Entity\AdditionalService;
use App\Repository\AdditionalServiceRepository;
use App\Serializer\AdditionalServiceCollectionDenormalizer;
use PHPUnit\Framework\TestCase;
use Dto\AdditionalServiceCollection;
class AddionalServiceCollectionDenormalizerTest extends TestCase
{
public function provider()
{
$expected = new AdditionalServiceCollection();
$expected->add(1, 22.1)->add(2, 3.1);
return [
[['man_1' => 22.1], $expected],
[['recycling' => 3.1], $expected],
];
}
/**
* #dataProvider provider
* #covers \App\Serializer\AdditionalServiceCollectionDenormalizer::denormalize
*/
public function testDenormalize(array $row, AdditionalServiceCollection $exptected)
{
$manOneService = $this->createMock(AdditionalService::class);
$manOneService->expects($this->any())->method('getId')->willReturn(1);
$recycling = $this->createMock(AdditionalService::class);
$recycling->expects($this->any())->method('getId')->willReturn(2);
$additionalServicesRepoMock = $this
->getMockBuilder(AdditionalServiceRepository::class)
->setMethods(['findOneBy'])
->disableOriginalConstructor()
->getMock();
$additionalServicesRepoMock
->expects($this->any())
->method('findOneBy')
->will($this->returnValueMap(
[
['name'=>['man_1'], $manOneService],
['name'=>['recycling'], $recycling],
]
));
$denormalizer = new AdditionalServiceCollectionDenormalizer($additionalServicesRepoMock);
self::assertEquals($exptected, $denormalizer->denormalize($row, AdditionalServiceCollection::class));
}
}
I had a hard time debugging the PHPUnit library, to figure out finally that it is the findOneBy() method that expects two arguments, among which the second one is optional (set to null)
The willReturnMap() method is as follows:
/**
* Stubs a method by returning a value from a map.
*/
class ReturnValueMap implements Stub
{
/**
* #var array
*/
private $valueMap;
public function __construct(array $valueMap)
{
$this->valueMap = $valueMap;
}
public function invoke(Invocation $invocation)
{
$parameterCount = \count($invocation->getParameters());
foreach ($this->valueMap as $map) {
if (!\is_array($map) || $parameterCount !== (\count($map) - 1)) {
continue;
}
$return = \array_pop($map);
if ($invocation->getParameters() === $map) {
return $return;
}
}
return;
}
I suspected the method was always returning with null because of the unmet condition $parameterCount !== (\count($map) - 1).
A breakpoint confirmed my doubts, and also revealed that $invocation->getParameters() dumps as follows:
array(2) {
[0] =>
array(1) {
'name' =>
string(5) "man_1"
}
[1] =>
NULL
}
Hence, I had to explicitely pass null as second argument.
So finally the working map had to be:
$this->additionalServicesRepoMock
->method('findOneBy')
->willReturnMap([
[['name' => 'man_1'], null, $manOneService],
[['name' => 'recycling'], null, $recyclingService],
]);
It looks like the parameter of returnValueMap() in testDenormalize() needs brackets to make it indexed array.
Here's a slightly modified version of code snippet from the PHPUnit's document:
<?php
namespace App\Tests;
use PHPUnit\Framework\TestCase;
class ReturnValueMapTest extends TestCase
{
public function testReturnValueMapWithAssociativeArray()
{
$stub = $this->createMock(SomeClass::class);
$map = [
[
'name' => ['man_1'],
'Hello'
],
];
$stub->method('doSomething')
->will($this->returnValueMap($map));
// This will fail as doSomething() returns null
$this->assertSame('Hello', $stub->doSomething(['name' => ['man_1']]));
}
public function testReturnValueMapWithIndexedArray()
{
$stub = $this->createMock(SomeClass::class);
$map = [
[
['name' => ['man_1']], // Notice the difference
'Hello'
],
];
$stub->method('doSomething')
->will($this->returnValueMap($map));
$this->assertSame('Hello', $stub->doSomething(['name' => ['man_1']]));
}
}
class SomeClass
{
public function doSomething()
{}
}
Related
I'm using the Symfony Serializer 3.3 bundle to convert and object to XML.
And I want boolean type returned as Y or N, instead of 1 or 0, and I don't want to change the accessor method.
Here's an example:
namespace Acme;
class Person
{
private $name;
private $enabled;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function isEnabled()
{
return $this->enabled;
}
public function setEnabled($enabled)
{
$this->enabled = $enabled;
}
}
$person = new Acme\Person();
$person->setName('foo');
$person->setEnabled(true);
$serializer->serialize($person, 'xml');
getting result:
<?xml version="1.0"?>
<response>
<name>foo</name>
<enabled>1</enabled> <!-- bad value -->
</response>
desired result:
<?xml version="1.0"?>
<response>
<name>foo</name>
<enabled>Y</enabled> <!-- goodvalue -->
</response>
You can register a new jms type formatted_boolean
<?php
declare(strict_types=1);
namespace App\Util\Serializer\Normalizer;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\XmlSerializationVisitor;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
class BoolHandler implements SubscribingHandlerInterface
{
public static function getSubscribingMethods(): array
{
return [
[
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => XmlEncoder::FORMAT,
'type' => 'formatted_boolean',
'method' => 'serializeToXml',
],
];
}
public function serializeToXml(
XmlSerializationVisitor $visitor,
$value,
array $type,
Context $context = null
) {
return $value ? 'Y' : 'N';
}
}
But in this case, you have to add #JMS\Type(name="formatted_boolean") for each boolean property
You can do this by event subscriber. It affects all boolean properties
<?php
declare(strict_types=1);
namespace App\EventListener\Serializer\Entity;
use JMS\Serializer\EventDispatcher\Events;
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use JMS\Serializer\Metadata\StaticPropertyMetadata;
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
class BoolSubscriber implements EventSubscriberInterface
{
/**
* #return array<array<string, mixed>>
*/
public static function getSubscribedEvents(): array
{
return [
[
'event' => Events::POST_SERIALIZE,
'method' => 'onPostSerialize',
'format' => XmlEncoder::FORMAT,
'priority' => 0,
],
];
}
public function onPostSerialize(ObjectEvent $event): void
{
$visitor = $event->getVisitor();
$class = get_class($event->getObject());
$reflectionExtractor = new ReflectionExtractor();
$properties = $reflectionExtractor->getProperties($class);
$propertyAccessor = new PropertyAccessor();
foreach ($properties as $property) {
$types = $reflectionExtractor->getTypes($class, $property);
$type = $types[0] ?? null;
if ($type instanceof Type && $type->getBuiltinType() == Type::BUILTIN_TYPE_BOOL) {
$metadata = new VirtualPropertyMetadata($class, $property);
if ($visitor->hasData($metadata->name)) {
$value = $propertyAccessor->getValue($event->getObject(), $property) ? 'Y' : 'N';
$visitor->visitProperty(
new StaticPropertyMetadata($class, $metadata->name, $value),
$value
);
}
}
}
}
}
In a Symfony 5.0.2 project a test of the new Mailer fails with
Error: Call to a member function getSubject() on null
The email service and test are based on symfonycast tutorials.
Adding var_dump($email); in the service immediately after $email = ...; shows object(Symfony\Bridge\Twig\Mime\TemplatedEmail)#24 (11) {..., which says there is a real object created in the service.
services.yaml:
App\Services\EmailerService:
$mailer: '#mailer'
$senderAddress: '%app.sender_address%'
Service:
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
class EmailerService
{
private $mailer;
private $sender;
public function __construct($mailer, $senderAddress)
{
$this->mailer = $mailer;
$this->sender = $senderAddress;
}
public function appMailer($mailParams)
{
$email = (new TemplatedEmail())
->from($this->sender)
->to($mailParams['recipient'])
->subject($mailParams['subject'])
->htmlTemplate($mailParams['view'])
->context($mailParams['context']);
$this->mailer->send($email);
}
}
Test:
use App\Services\EmailerService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Mailer\MailerInterface;
class MailerTest extends TestCase
{
public function testSimpleMessage()
{
$symfonyMailer = $this->createMock(MailerInterface::class);
$symfonyMailer->expects($this->once())
->method('send');
$mailer = new EmailerService($symfonyMailer, 'admin#bogus.info', 'admin#bogus.info');
$mailParams = [
'view' => 'Email/non_user_forgotten_password.html.twig',
'context' => ['supportEmail' => 'admin#bogus.info'],
'recipient' => 'bborko#bogus.info',
'subject' => 'Test message',
];
$email = $mailer->appMailer($mailParams);
$this->assertSame('Test message', $email->getSubject());
}
}
appMailer() must return a TemplatedEmail object so you can call getSubject() on it. Currently it is returning nothing. Change it to:
public function appMailer($mailParams)
{
$email = (new TemplatedEmail())
->from($this->sender)
->to($mailParams['recipient'])
->subject($mailParams['subject'])
->htmlTemplate($mailParams['view'])
->context($mailParams['context']);
$this->mailer->send($email);
return $email; // I added this line.
}
I have two objects with a Parent-Child relationship. For each object I have a custom normalizer as can be seen below:
ChildNormalizer.php
use Symfony\Component\Serializer\Normalizer\scalar;
class ChildNormalizer
{
public function normalize($object, $format = null, array $context = array())
{
return [
"name" => $object->getName(),
"age" => $object->getAge(),
...
];
}
public function supportsNormalization($data, $format = null)
{
return ($data instanceof Child) && is_object($data);
}
}
ParentNormalizer.php
use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\scalar;
class ParentNormalizer implements NormalizerInterface, NormalizationAwareInterface
{
use NormalizerAwareTrait;
public function normalize($object, $format = null, array $context = array())
{
return [
...
"children" => array_map(
function ($child) use ($format, $context) {
return $this->normalizer->normalize($child);
}, $object->getChildren()
),
...
];
}
public function supportsNormalization($data, $format = null)
{
return ($data instanceof Parent) && is_object($data);
}
}
When I try to serialize the Parent object which in turn normalizes the Child object I get the following exception:
Call to a member function normalize() on null
Have I missed a configuration step, or what the hell am I doing wrong?
Solved the issue, I was implementing the wrong *AwareInterface.
The code works perfectly if the ParentNormalizer implements NormalizerAwareInterface rather than NormalizationAwareInterface.
use Symfony\Component\Serializer\Encoder\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\scalar;
class ParentNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
...
}
I need to validate an email passed by user:
private function validate($value): bool
{
$violations = $this->validator->validate($value, [
new Assert\NotBlank(),
new Assert\Email(),
new UniqueEntity([
'entityClass' => User::class,
'fields' => 'email',
])
]);
return count($violations) === 0;
}
But UniqueEntity constraint throws an exception:
Warning: get_class() expects parameter 1 to be object, string given
Seems like ValidatorInterface::validate() method's first argument awaiting for Entity object with getEmail() method, but it looks ugly.
Is there any elegant way to validate uniqueness of field passing only scalar value to ValidatorInterface::validate() method?
Seems like there is no built-in Symfony solution to do what I want, so I created custom constraint as Jakub Matczak suggested.
UPD: This solution throws a validation error when you're sending form to edit your entity. To avoid this behavior you'll need to improve this constraint manually.
Constraint:
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class UniqueValueInEntity extends Constraint
{
public $message = 'This value is already used.';
public $entityClass;
public $field;
public function getRequiredOptions()
{
return ['entityClass', 'field'];
}
public function getTargets()
{
return self::PROPERTY_CONSTRAINT;
}
public function validatedBy()
{
return get_class($this).'Validator';
}
}
Validator:
namespace AppBundle\Validator\Constraints;
use Doctrine\ORM\EntityManager;
use InvalidArgumentException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class UniqueValueInEntityValidator extends ConstraintValidator
{
/**
* #var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function validate($value, Constraint $constraint)
{
$entityRepository = $this->em->getRepository($constraint->entityClass);
if (!is_scalar($constraint->field)) {
throw new InvalidArgumentException('"field" parameter should be any scalar type');
}
$searchResults = $entityRepository->findBy([
$constraint->field => $value
]);
if (count($searchResults) > 0) {
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Service:
services:
app.validator.unique_value_in_entity:
class: AppBundle\Validator\Constraints\UniqueValueInEntityValidator
arguments: ['#doctrine.orm.entity_manager']
tags:
- { name: validator.constraint_validator }
Usage example:
private function validate($value): bool
{
$violations = $this->validator->validate($value, [
new Assert\NotBlank(),
new Assert\Email(),
new UniqueValueInEntity([
'entityClass' => User::class,
'field' => 'email',
])
]);
return count($violations) === 0;
}
For this porpose i would use #UniqueEntity(fields={"email"}) in user class annotation. Kind of this way:
/**
* #ORM\Entity()
* #ORM\Table(name="user")
* #UniqueEntity(fields={"email"})
*/
I use JMS Serializer Bundle and Symfony2. I am using VirtualProperties. currently, I set the name of a property using the SerializedName annotation.
/**
* #JMS\VirtualProperty()
* #JMS\SerializedName("SOME_NAME")
*/
public function getSomething()
{
return $this->something
}
Is it possible to set the serialized name dynamically inside the function? Or is it possible to dynamically influence the name using Post/Pre serialization events?
Thanks!
I don't think you can do this directly, but you could accomplish something similar by having several virtual properties, one for each possible name. If the name is not relevant to a particular entity, have the method return null, and disable null serialization in the JMS config.
In the moment when you go to serialize the object, do the following:
$this->serializer = SerializerBuilder::create()->setPropertyNamingStrategy(new IdenticalPropertyNamingStrategy())->build();
$json = $this->serializer->serialize($object, 'json');
dump($json);
Entity
/**
* #JMS\VirtualProperty("something", exp="context", options={
* #JMS\Expose,
* })
*/
class SomeEntity
{
}
Event Listener
abstract class AbstractEntitySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
[
'event' => Events::POST_SERIALIZE,
'method' => 'onPostSerialize',
'class' => static::getClassName(),
'format' => JsonEncoder::FORMAT,
'priority' => 0,
],
];
}
public function onPostSerialize(ObjectEvent $event): void
{
foreach ($this->getMethodNames() as $methodName) {
$visitor = $event->getVisitor();
$metadata = new VirtualPropertyMetadata(static::getClassName(), $methodName);
if ($visitor->hasData($metadata->name)) {
$value = $this->{$methodName}($event->getObject());
$visitor->visitProperty(
new StaticPropertyMetadata(static::getClassName(), $metadata->name, $value),
$value
);
}
}
}
abstract protected static function getClassName(): string;
abstract protected function getMethodNames(): array;
}
...
class SomeEntitySubscriber extends AbstractEntitySubscriber
{
protected static function getClassName(): string
{
return SomeEntity::class;
}
protected function getMethodNames(): array
{
return ['getSomething'];
}
protected function getSomething(SomeEntity $someEntity)
{
return 'some text';
}
}