How to automatically add a property on every Entities - symfony

It's been days that I'm trying to achieve the following :
Add a deletedAt property on every entities by default without having to use a trait, or adding inheritance (or mapped superclasses) to all of my already created entities.
The goal here is to also make it work with doctrine migration script so that it would automatically add the associated column in every tables.
Tried to subscribe to the Doctrine\ORM\Events::loadClassMetadata event this way
class ClassMetadataEventSubscriber implements EventSubscriber
{
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata
];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $args) {
$metadata = $args->getClassMetadata();
$metadata->mapField([
'fieldName' => 'deletedAt',
'type' => 'datetime',
'nullable' => true,
'columnName' => 'deleted_at'
]);
}
}
But I get the ReflectionProperty::_construct() Exception
App\Entity\Etudiant::$deletedAt does not exist
Exception trace:
at /srv/api/vendor/doctrine/persistence/lib/Doctrine/Persistence/Mapping/RuntimeReflectionService.php:90
ReflectionProperty->__construct()
Doctrine\Persistence\Mapping\RuntimeReflectionService->getAccessibleProperty()
Doctrine\ORM\Mapping\ClassMetadataInfo->wakeupReflection()
Doctrine\ORM\Mapping\ClassMetadataFactory->wakeupReflection()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->loadMetadata()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->getMetadataFor()
Doctrine\Persistence\Mapping\AbstractClassMetadataFactory->getAllMetadata()
Doctrine\Migrations\Provider\OrmSchemaProvider->createSchema()
Doctrine\Migrations\Generator\DiffGenerator->createToSchema()
Doctrine\Migrations\Generator\DiffGenerator->generate()
Doctrine\Migrations\Tools\Console\Command\DiffCommand->execute()
Also, the deletedAt property is here to enable soft deletion through gedmo/doctrine-extensions

Related

How to override the PUT operation update process in the Api Platform

I'm trying to override the PUT operation to perform my actions under certain conditions. That is, if the sent object is different from the original object (from the database), then I need to create a new object and return it without changing the original object.
Now when I execute the query I get a new object, as expected, but the problem is that the original object also changes
Entity
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
new Post(controller: CreateAction::class),
new Put(processor: EntityStateProcessor::class),
],
paginationEnabled: false
)]
class Entity
EntityStateProcessor
final class PageStateProcessor implements ProcessorInterface
{
private ProcessorInterface $decorated;
private EntityCompare $entityCompare;
public function __construct(ProcessorInterface $decorated, EntityCompare $entityCompare)
{
$this->decorated = $decorated;
$this->entityCompare = $entityCompare;
}
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$new_entity = clone $data; // (without id)
// do something with new entity
return $this->decorated->process($new_entity, $operation, $uriVariables, $context);
}
return $data;
}
}
I don't understand why this happens, so I return a clone of the original object to the process. It would be great if someone could tell me what my mistake is.
I also tried the following before returning the process
$this->entityManager->refresh($data); - Here I assumed that the original instance of the object will be updated with data from the database and the object will not be updated with data from the query
$this->entityManager->getUnitOfWork()->detach($data); - Here I assumed that the object would cease to be manageable and would not be updated
But in both cases the state of the original $data changes.
I'm using ApiPlatform 3.0.2
The error is that the main entity is related to an additional entity, so it's not enough to detach the main entity from UnitOfWork. So use the Doctrine\ORM\UnitOfWork->clear(YourEntity::class) method to detach all instances of the entity, and you do the same for relationships.
Once the entity is detach, cloning the entity becomes pointless because the previous entity instance isn't managed by the Doctrine ORM, so my code rearranges itself like this:
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
if (($this->entityCompare)($data)) { // checking for object changes
$this->getEntityManager()->getUnitOfWork()->clear(Entity::class);
$this->getEntityManager()->getUnitOfWork()->clear(EelatedEntity::class);
// do something with new entity
return $this->decorated->process($data, $operation, $uriVariables, $context);
}
return $data;
}

How to denormalize an array recursively in Symfony 5?

I am currently trying to denormalize an array, which came out of an API as a JSON response and was JSON decoded.
The problem is, that I want it to be denormalized into a class and one of the properties is another class.
It feels like it should be possible to get such an easy job done with the Symfony denormalizer, but I always get the following exception:
Failed to denormalize attribute "inner_property" value for class "App\Model\Api\Outer": Expected argument of type "App\Model\Api\Inner", "array" given at property path "inner_property".
My denormalizing code looks like that:
$this->denormalizer->denormalize($jsonOuter, Outer::class);
The denormalizer is injected in the constructor:
public function __construct(DenormalizerInterface $denormalizer) {
The array I try to denormalize:
array (
'inner_property' =>
array (
'property' => '12345',
),
)
Finally the both classes I try to denormalize to:
class Outer
{
/** #var InnerProperty */
private $innerProperty;
public function getInnerProperty(): InnerProperty
{
return $this->innerProperty;
}
public function setInnerProperty(InnerProperty $innerProperty): void
{
$this->innerProperty = $innerProperty;
}
}
class InnerProperty
{
private $property;
public function getProperty(): string
{
return $this->property;
}
public function setProperty(string $property): void
{
$this->property = $property;
}
}
After hours of searching I finally found the reason. The problem was the combination of the "inner_property" snake case and $innerProperty or getInnerProperty camel case. In Symfony 5 the camel case to snake case converter is not enabled by default.
So I had to do this by adding this config in the config/packages/framework.yaml:
framework:
serializer:
name_converter: 'serializer.name_converter.camel_case_to_snake_case'
Here is the reference to the Symfony documentation: https://symfony.com/doc/current/serializer.html#enabling-a-name-converter
Alternatively I could have also add a SerializedName annotation to the property in the Outer class:
https://symfony.com/doc/current/components/serializer.html#configure-name-conversion-using-metadata
PS: My question was not asked properly, because I didn't changed the property and class names properly. So I fixed that in the question for future visitors.

Unable to access the Roles from Authorize Attribute in IActionModelConvention implemented Apply Method

I am trying here to parse the Roles from Action Method Applied Authorize filter. But I am only able to get in Runtime through a quick watch. But Unable to get it via LINQ query.
internal class AuthorizeModelConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
IReadOnlyList<object> actionAttributes = action.Attributes;
if (!actionAttributes.Select(x => x.GetType()).Any(x => x.Name.Contains("AllowAnonymousAttribute")))
{
action.Filters.Add(new AuthorizeFilter("auth-policy"));
}
}
}
According to your description, I suggest you could try to use below codes to check if the authorize attribute's roles value is "admin" or not.
actionAttributes.Select(t => t.GetType().GetProperties().ToList().Select(x => x.GetValue(t, null)).ToList()).Any(x => x.Contains("admin"));
Result:

How to share variables to all views (including behavior) in twig?

I have this controller action:
public function index(Request $request)
{
$start = $request->get('start', 0);
$limit = $request->get('limit', 10);
$articles = $this->articleRepository->all($start, $limit);
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
return $this->render('article/index.html.twig', [
'articles' => $articles,
'navigation'=>$navigation
]);
}
I build a menu with:
$navigation = $this->menu->build()->render(new RenderStrategyBootstrap4());
Now this is high level behavior and I do not want to render this for every action there is. Is there a way in Symfony to move this behavior to a sort of view composer (like in Laravel?) and then share the variable with the view?
Or is there no way and do I need to create a base controller?
You could create a Custom Twig Extension as described here: https://symfony.com/doc/current/templating/twig_extension.html
There you can register a custom Twig Function like this:
public function getFunctions()
{
return array('renderNavigation' => new TwigFunction(
'renderNavigation',
array($this, 'renderNavigation'),
array('needs_environment' => true, 'is_safe' => array('html'))
);
}
public function renderNavigation(Environment $twig)
{
/* ... */
return $twig->render(/* ... */);
}
Then you can use the function in every template like {{ renderNavigation() }}
Since the Twig Extension itself is a service you can inject whatever service else you need (like RequestStack, EntityManager and so on) and even cache expensive operations within the extension if you need to function to be run more than once.

Dynamic validation groups in Symfony2

I need to implement form validation depending on submitted data. While data object's invoice property is true then validation_groups array should contain not only 'add' validation but also 'company'.
I've found "Groups based on Submitted Data" chapter in Symfony Docs https://github.com/symfony/symfony-docs/blob/master/book/forms.rst.
The problem is that :
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Strict\PublicBundle\Entity\Booking',
'validation_groups' => function(FormInterface $form)
{
return array('booking');
},
);
}
throws this error:
Warning: Illegal offset type in /var/www/vendor/symfony/src/Symfony/Component/Validator/GraphWalker.php line 101
500 Internal Server Error - ErrorException
Any ideas what can be wrong?
According to this pull request using callbacks for validation_groups will be posible in Symfony 2.1 (not yet released, currently master branch).
Are you sure you are using master branch? If you are using current stable (2.0.x), it has no support for Groups based on Submitted Data, you have to use arrays only. See proper documentation on http://symfony.com/doc/current/book/forms.html#book-forms-validation-groups.
I've got an alternative: If you're able to determine the condition prior to binding the form, you can simply override the default list of validation groups when you create the form.
In my case I've got an order object in session that gets updated across multiple form pages.
Order can be "Delivery" or "Pickup" and if delivery is selected on a previous screen I need to validate address details on this screen:
if ($order->getOrderType() == "Delivery")
{
$validationGroups = array('step3', 'delivery');
}
else
{
$validationGroups = array('step3');
}
$formType = new Form\Order3Type();
$form = $this->createForm($formType, $order, array("validation_groups" => $validationGroups));
$form->bindRequest($request);
If your condition is in the form and not already in session, you could always just pull the value straight from the request object.
// MyFormType.php
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'Strict\PublicBundle\Entity\Booking',
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
$groups = ['booking'];
if ($data->invoice) {
$groups[] = 'company';
}
return $groups;
},
]);
}

Resources