Dynamic validation groups in Symfony2 - symfony

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;
},
]);
}

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 automatically add a property on every Entities

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

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.

Doctrine Mongodb getOriginalDocumentData on embedded document

In my symfony application i've got my event_subscriber
CoreBundle\EventSubscriber\CloseIssueSubscriber:
tags:
- { name: doctrine_mongodb.odm.event_subscriber, connection: default }
My subscriber simply listen to postPersist and postUpdate events:
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
);
}
public function postPersist(LifecycleEventArgs $args)
{
$this->index($args);
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->index($args);
}
In my index function what I need to do is to get if certain field has changed in particular the issue.status field.
public function index(LifecycleEventArgs $args)
{
$document = $args->getEntity();
$originalData = $uow->getOriginalDocumentData($document);
$originalStatus = $originalData && !empty($originalData['issue']) ? $originalData['issue']->getStatus() : null;
var_dump($originalStatus);
var_dump($document->getIssue()->getStatus());die;
}
In my test what I do is change the issue.status field so I expect to receive 2 different values from the var_dump but instead I got the last status from both.
My document is simply something like that:
class Payload
{
/**
* #ODM\Id
*/
private $id;
/**
* #ODM\EmbedOne(targetDocument="CoreBundle\Document\Issue\Issue")
* #Type("CoreBundle\Document\Issue\Issue")
*/
protected $issue;
}
In the embedded issue document status is simply a text field.
I've also try to use the changeset:
$changeset = $uow->getDocumentChangeSet($document);
foreach ($changeset as $fieldName => $change) {
list($old, $new) = $change;
}
var_dump($old->getStatus());
var_dump($new->getStatus());
Also this two var_dumps returns the same status.
By the time of postUpdate changes in the document are already done so originalDocumentData is adjusted and ready for new calculations. Instead you should hook into preUpdate event and use $uow->getDocumentChangeSet($document); there.
I guess that you want to run index once changes have been written to the database, so on preUpdate you can accumulate changes in the listener and additionally hook into postFlush event to re-index documents.
I found the solution to my problem.
What malarzm said in the other answer is correct but not the solution to my problem.
I suppose that I get only one postUpdate/preUpdate postPersist/prePersist just for the Document (Payload) instead I notice that it get called event for the embedded document (don't know why doctrine consider it a persist).
So the main problem is that I'm waiting for a Payload object instead I have to wait for a Issue object.
In other hands I was unable to use the getOriginalDocumentData work right even in the postUpdate and in the preUpdate so I have to use the getDocumentChangeSet().

Having issues getting the notification feed for new follows when using stream-laravel

I am building a small project using Getstream Laravel package. However I am having a problem trying to display notifications for new followers. I get an empty result set when I call \FeedManager::getNotificationFeed($request->user()->id)->getActivities() in a controller method. I have my follow model looking like this:
class Follow extends Model
{
protected $fillable = ['target_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function target()
{
return $this->belongsTo(User::class);
}
public function activityNotify()
{
$targetFeed = \FeedManager::getNotificationFeed($this->target->id);
return array($targetFeed);
}
}
Then the controller action to get the notifications for new follows looks like this:
public function notification(Request $request)
{
$feed = \FeedManager::getNotificationFeed($request->user()->id);
dd($feed->getActivities());
$activities = $feed->getActivities(0,25)['results'];
return view('feed.notifications', [
'activities' => $activities,
]);
}
In the user model I have defined a relationship that a user has many follows. And lastly the follow and unfollow actions in the FollowController look like this:
public function follow(Request $request)
{
// Create a new follow instance for the authenticated user
// This target_id will come from a hidden field input after clicking the
// follow button
$request->user()->follows()->create([
'target_id' => $request->target_id,
]);
\FeedManager::followUser($request->user()->id, $request->target_id);
return redirect()->back();
}
public function unfollow($user_id, Request $request)
{
$follow = $request->user()->follows()->where('target_id', $user_id)->first();
\FeedManager::unfollowUser($request->user()->id, $follow->target_id);
$follow->delete();
return redirect()->back();
}
Not sure if there's something I left out but I can't get results for the notification feed. If I head over to the explorer tab from the Stream dashboard, I can see that I got two new follows which generated both timeline and timeline_aggregated type of feed. Or how should I get the notification feed from a controller action? Thanks in advance
The \FeedManager::followUser method create a two follow relationships: timeline to user and timeline_aggregated to user.
In this case what you want to create a follow relationship between notification and user. Something like this should do it:
\FeedManager:: getNotificationFeed($request->user()->id)
.followFeed('user', $follow->target_id)

Resources