How to update the mapping information of an entity - php

I would like to change the default mapping of the Product entity from the Sylius ProductBundle. So I created a listener to the ClassMetadata Event:
<?php
namespace App\Symfony\EventListener\Sylius;
use Sylius\Component\Product\Model\Product;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
/**
* Remove the reference of a Variant from its parent (Product) without delete it
*/
class ProductLoadMetadataSubscriber implements EventSubscriber
{
/**
* #return array
*/
public function getSubscribedEvents()
{
return array(
'loadClassMetadata',
);
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/** #var ClassMetadata $metadata */
$metadata = $eventArgs->getClassMetadata();
if (Product::class !== $metadata->name) {
return;
}
// Property "variants" in "Sylius\Component\Product\Model\Product" was already declared, but it must be declared only once
$metadata->mapOneToMany([
'fieldName' => 'variants',
'targetEntity' => 'Sylius\Component\Product\Model\VariantInterface',
'mappedBy' => 'object',
'orphanRemoval' => false
]);
//$variantsMapping = $metadata->getAssociationMapping('variants');
//$variantsMapping['orphanRemoval'] = false;
}
}
My goal is to set the orphanRemoval option value to false for the variants field.
But I didn't find a method or something else to update the ClassMetadataInfo instance.
At the moment I have this error:
Property "variants" in "Sylius\Component\Product\Model\Product" was
already declared, but it must be declared only once
It's logical because the variants field already exists.
EDIT:
I tried another thing but without success:
unset($metadata->associationMappings['variants']['orphanRemoval']);
// setAssociationOverride doesn't handle 'orphanRemoval' but it calls internally _validateAndCompleteOneToManyMapping
// which set orphanRemoval to false by default
$metadata->setAssociationOverride('variants', []);

To set orphanRemoval, you can try the following snippet:
if (SomeClass::class === $metadata->getName()) {
if (isset($metadata->associationMappings['itemShippingTrackings'])) {
$metadata->associationMappings['itemShippingTrackings']['orphanRemoval'] = false;
}
}

Related

Deserialize single string property into object in symfony JMS PHP

Having PHP, Symfony 4.4, JMS Serializer and json payload (request body) as such:
{
"quantity": 1,
"product": {
"sku": "bla"
},
"myId": {
"id": "0010N00005GcOhhQAF"
}
}
This payload is send to my endpoint and everything is correctly deserialized into correct resulting CustomRequest object - also nested objects like Product and MyId is correctly created. In the case of the Product object it is ok, because Product have complex structure with multiple attributes.
But what I would like to achieve is to make the input of myId easier. I would like to have it instead of:
"myId": {
"id": "0010N00005GcOhhQAF"
}
having simple this:
"myId": "0010N00005GcOhhQAF"
You may be asking why do I have class for simple id. It is not simple id, it have some special validation and bussiness logic inside and is used through out all application, so for validation purposes its better to have an object for it.
So lets say, I want my deserializer to automatically take that simple id string into the constructor of the class MyId, and return the object. MyId class is simple class like:
class MyId
{
/**
* #AppAssert\MyId()
*/
private ?string $value = null;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString()
{
return $this->value;
}
}
FYI: I tried this annotation in the resulting CustomRequest object, but it was not working
/**
* #Serializer\Type("App\Model\MyId")
*/
private ?MyId $myId = null;
EDIT: Another important part: this is how the endpoint automatically transforms request body into the CustomRequest object. Here you can see, that I am using ParamConverter for it.
/**
* #Rest\Post("/product")
* #ParamConverter("customRequest", options={
* "validator"={"groups"={"Default","product"="create"}},
* })
*/
public function postCreateProductAction(CustomRequest $customRequest) {
// ...
}
The question is: How to use JMS Serializer with Symfony to make it work. To take simple one string pass it atomatically to constructor and make and object from it. Is this even possible? Thanks
You need to write custom (De)Normalizer.
What is it: https://symfony.com/doc/current/components/serializer.html#normalizers
How to customize: https://symfony.com/doc/current/serializer/custom_normalizer.html
I made it working by using JMS\Serializer\Handler\SubscribingHandlerInterface. With this approach you can just simply add callbacks that kicks in during serialization/deserialization process. See code below that shows exact solution for MyId object.
More info about this technique here: https://jmsyst.com/libs/serializer/master/handlers
namespace App\Handlers;
use App\Model\MyId;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\JsonSerializationVisitor;
class MyIdHandler implements SubscribingHandlerInterface
{
/**
* #return array<int, array<string, int|string>>
*/
public static function getSubscribingMethods(): array
{
return [
[
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => MyId::class,
'method' => 'serializeMyIdToJson',
],
[
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => MyId::class,
'method' => 'deserializeMyIdFromJson',
],
];
}
/**
* #param array<mixed> $type
*/
public function serializeMyIdToJson(JsonSerializationVisitor $visitor, ?MyId $myId, array $type, Context $context): string
{
if ($myId === null) {
return '';
}
return (string)$myId;
}
/**
* #param array<mixed> $type
*/
public function deserializeMyIdFromJson(JsonDeserializationVisitor $visitor, string $myId, array $type, Context $context): ?MyId
{
if (empty($myId)) {
return null;
}
return new MyId($myId);
}
}

filter entity fields on symfony controller

How can I choose(filter) on my controller which fields I want (or don't want) to pass to my frontend?
my Controller:
/**
* #Route("/", name="dashboard")
*/
public function index()
{
$aniversariantes = $this->getDoctrine()->getRepository(Usuario::class)->aniversariantes();
return $this->render('dashboard/index.html.twig', [
'controller_name' => 'DashboardController',
'aniversariantes' => $aniversariantes
]);
}
My repository:
/**
* #return []
*/
public function aniversariantes(): array
{
$qb = $this->createQueryBuilder('u')
->andWhere('u.ativo = 1')
->andwhere('extract(month from u.dtNascimento) = :hoje')
->setParameter('hoje', date('m'))
->getQuery();
return $qb->execute();
}
Dump from entity:
What can I do if I don't want to pass the "password" field for example?
If you are just trying to prevent certain fields from being dumped, it is useful to know
Internally, Twig uses the PHP var_dump function.
https://twig.symfony.com/doc/2.x/functions/dump.html
This means you can can define the PHP magic method __debugInfo in your entity
This method is called by var_dump() when dumping an object to get the properties that should be shown. If the method isn't defined on an object, then all public, protected and private properties will be shown.
https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
So in your entity do something like this:
class Usuario {
...
public function __debugInfo() {
return [
// add index for every field you want to be dumped
// assign/manipulate values the way you want it dumped
'id' => $this->id,
'nome' => $this->nome,
'dtCadastro' => $this->dtCadastro->format('Y-m-d H:i:s'),
];
}
...
}

Symfony - get an entity object in a field constraint class

I created my custom constraint validator:
class CustomConstraint extends Constraint
{
public $message = '';
}
class CustomConstraintValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
exit($this->context->getObject()); // returns null
}
}
In docs it is stated that:
Returns the currently validated object.
, but for me it returns NULL instead.
P.S. I do not want to assign this constraint to Entity, only to certain forms or fields.
My form property which is validated:
->add('rejectReasons', null, array(
'property' => 'name',
'multiple' => true,
'constraints' => array(
new CustomConstraint(array(
'message' => 'Application can not be refused.'
)),
)
));
Property in entity:
/**
* #ORM\ManyToMany(targetEntity="RejectReason")
* #ORM\JoinTable(name="relationship_application_reject_reasons",
* joinColumns={#ORM\JoinColumn(name="application_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="reject_reason_id", referencedColumnName="id")}
* )
*/
private $rejectReasons;
UPDATE
I tried putting constraint on other string property, I still get NULL.
Look at ExecutionContextInterface it says that:
getObject() Returns the currently validated object.
If the validator is currently validating a class constraint, the
object of that class is returned. If it is a validating a property or
getter constraint, the object that the property/getter belongs to is
returned.
In other cases, null is returned.
So as you can see, you have to assign to a class or a property or getter. Otherwise you will get null.
For those making form validation using dependencies itself this can help.
I assume that the Symfony version is 3.4 or 4.1 and you have symfony/form on your project.
Build your CustomConstraintValidator
The best way to deal with Symfony Form Validators with some kind of dependency are using CustomValidators
Above is a example that I use to work with them.
Supposed that we have an Entity like
// src/Entity/myEntity.php
namespace App\Entity;
...
class myEntity
{
private $id;
private $name; // string, required
private $canDrive; // bool, not required (default=false)
private $driveLicense; // string, not required (default = null)
public function __construct()
{
$this->canDrive = false;
}
// getters and setters
}
We don't need to populate $driveLicense (cause the attribute its not mandatory), but if $canDrivechange from false to true, now $driveLicense must have a value.
$driveLicense is $canDrive dependent.
To build a form for that and validate $driveLicense correctly on the FormType (the best practice) we need to build a CustomConstraintValidator.
Building CanDriveValidator
// src/Validator/Constraints/CanDrive.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class CanDrive extends Constraint
{
public $message = 'invalid_candrive_args'; // I like translators :D
}
Translator file - optional
//src/translators/validators.en.yaml //
invalid_candrive_args: When "{{ candrivelabel }} " field is checked you must fill "{{ drivelicenselabel }}"
The validator
// src/Validator/Constraints/CanDriveValidator.php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CanDriveValidator extends ConstraintValidator
{
/**
* Checks if the passed value is valid.
*
* #param mixed $value The value that should be validated
* #param Constraint $constraint The constraint for the validation
*/
public function validate($value, Constraint $constraint)
{
$canDriveField = $this->context->getObject(); // the Field using this validator
$form = $canDriveField->getParent(); // the formType where the Field reside
$myEntity = $form->getData(); // The Entity mapped by formType
if ($myEntity->getCanDrive() == true && $myEntity->getDriveLicense() == null) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ candrivelabel }}', 'Can Drive')
->setParameter('{{ drivelicenselabel }}', 'Drive License')
->addViolation();
}
}
}
The form myEntityType
//src/Form/myEntityType.php
namespace App\Form;
use App\Entity\myEntity;
use App\Validator\Constraints\CanDrive;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class myEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name')
->add('canDrive', CheckBoxType::class, [
'required' => false,
'constraints' => array(new canDrive()),
]
)
->add('driveLicense', TextType::class, ['required' => false])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => myEntity::class]);
}
}
Now, when use call isValid() method of myEntityType form and the canDrive field was checked and driveLicense is blank, a Violation will be fired on canDrive field. If canDrive is set to false (not checked, not submitted), nothing happens and form will be valid even when driveLicense is blank.
If you are develep a Class Constraint Validator remember to add the getTargets method as example:
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
As described here in the doc
The answer is simple. Write :
this->context->getRoot()->getData()
and u have the object.

How to configure entities in zf2 for ManyToOne unidirectional doctrine mapping

I'm having trouble developing a form in zf2 using a doctrine ManyToOne unidirectional relationship. My entities look like this:
namespace AdminMyPages\Entity;
class MyPageItem
{
// ...
/**
* #ORM\ManyToOne(targetEntity="MyMessage")
* #ORM\JoinColumn(name="myMessageID", referencedColumnName="myMessageID")
**/
private $myMessage;
// ...
/**
* Allow null to remove association
*
* #param Collection $myMessage
*/
public function setMyMessage(Collection $myMessage = null)
{
$this->myMessage = $myMessage;
}
/**
* #return myMessage
*/
public function getMyMessage()
{
return $this->myMessage;
}
}
class MyMessage
{
// ...
}
The fieldset for MyPageItemFieldset looks like this:
namespace AdminMyPages\Form;
class MyPageItemFieldset extends Fieldset implements InputFilterProviderInterface
{
public function __construct(ObjectManager $objectManager)
{
parent::__construct('mypage-item-fieldset');
$this->setHydrator(new DoctrineHydrator($objectManager, 'AdminMyPages\Entity\MyPageItem'))
->setObject(new MyPageItem());
// ...
$myMessageFieldset = new MyMessageFieldset($objectManager);
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'myMessage',
'options' => array(
'count' => 1,
'target_element' => $myMessageFieldset
)
));
}
public function getInputFilterSpecification()
{
// ...
return array(
'myMessage' => array(
'required' => false
),
);
}
}
With this configuration I am able to "get" data from the MyMessage through getMyMessage(), so I know that the tables have been joined. However, when I try to bind the entity in a form, I get an error:
File:
C:\xampp\htdocs\GetOut\vendor\zendframework\zendframework\library\Zend\Form\Element\Collection.php:167
Message:
Zend\Form\Element\Collection::setObject expects an array or Traversable object argument; received "DoctrineORMModule\Proxy\__CG__\AdminMyPages\Entity\MyMessage"
Stack trace:
#0 ... Zend\Form\Element\Collection->setObject(Object(DoctrineORMModule\Proxy\__CG__\AdminMyPages\Entity\MyMessage))
...
One thought I have is that, since the ManyToOne relationship will only produce a single match, the MyMessage fieldset is hardly a collection - it's just one item - so Zend\Form\Element\Collection might not be the right form element to use. But, if it's not a collection, what is it?
You are totally right. Your MyMessage should not be a Collection. It should simply be an instance of MyMessage.
You defined a ManyToOne between MyPageItem and MyMessage meaning one page item has one message and a message has many page items. But since it is a unidirectional relationship that last part is never defined.
So the setter should look like this:
/**
* #param MyMessage $myMessage
*/
public function setMyMessage(MyMessage $myMessage = null)
{
$this->myMessage = $myMessage;
}
And you should also change your form field definition to a single MyMessage item.

ZF2 How to use global variables in the view

In ZF1 I used to declare variables in the application.ini
brandname = "Example"
weburl = "http://www.example.com/"
assetsurl = "http://assets.example.com/"
And in the Bootstrap I did this so i could access them in the view
define('BRANDNAME', $this->getApplication()->getOption("brandname"));
define('WEBURL', $this->getApplication()->getOption("weburl"));
define('ASSETSURL', $this->getApplication()->getOption("assetsurl"));
Whats the ZF2 way to do this, I know that i can create an array in the local.php config file like:
return array(
'example' => array(
'brandname' => 'Example',
'weburl' => 'http://www.example.com/',
'asseturl' => 'http://assets.example.com/',
),
);
When I want to access that variable in the controller I can do
$config = $this->getServiceLocator()->get('Config');
$config['example']['brandname']);
So far so good... but how do i access this variable in the view?
I don't want to create a view variable for it in every controller. And when i try the above in a view phtml file i get an error.
Zend\View\HelperPluginManager::get was unable to fetch or create an instance for getServiceLocator
Any ideas?
You could create a sinmple view helper to act as a proxy for your config, (totally un tested).
Module.php
public function getViewHelperConfig()
{
return array(
'factories' => array(
'configItem' => function ($helperPluginManager) {
$serviceLocator = $helperPluginManager->getServiceLocator();
$viewHelper = new View\Helper\ConfigItem();
$viewHelper->setServiceLocator($serviceLocator);
return $viewHelper;
}
),
);
}
ConfigItem.php
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceManager;
/**
* Returns total value (with tax)
*
*/
class ConfigItem extends AbstractHelper
{
/**
* Service Locator
* #var ServiceManager
*/
protected $serviceLocator;
/**
* __invoke
*
* #access public
* #param string
* #return String
*/
public function __invoke($value)
{
$config = $this->serviceLocator->get('config');
if(isset($config[$value])) {
return $config[$value];
}
return NULL;
// we could return a default value, or throw exception etc here
}
/**
* Setter for $serviceLocator
* #param ServiceManager $serviceLocator
*/
public function setServiceLocator(ServiceManager $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
}
You could then do something like this in your view, assuming you have them set in your config of course :)
echo $this->configItem('config_key');
echo $this->configItem('web_url');
I would personally tend to just pass the values through to the view every time though, keeping the view a dumb as possible.
I answered this before on a different post.
/* Inside your action controller method */
// Passing Var Data to Your Layout
$this->layout()->setVariable('stack', 'overflow');
// Passing Var Data to Your Template
$viewModel = new ViewModel(array( 'stack' => 'overflow' ));
/* In Either layout.phtml or {Your Template File}.phtml */
echo $this->stack; // Will print overview
That's it... No need to mess with view helpers, event manager, service manager, or anything else.
Enjoy!

Categories