How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field? - symfony

As the title says, I am trying to make a run-time decision on whether or not to include fields in the serialization. In my case, this decision will be based on permissions.
I am using Symfony 2, so what I'm looking to do is add an additional annotation called #ExcludeIf which accepts a security expression.
I can handle the annotation parsing and storing of the meta data, but I am not able to see how to integrate a custom exclusion strategy with the library.
Any suggestions?
Note: exclusion strategies are an actual construct in the JMS codebase, I just haven't been able to figure out the best way to integrate an extra on top of the others
PS: I had asked about this before and was pointed to using groups. For various reasons this is a very poor solution for my needs.

You just have to create a class that implements JMS\Serializer\Exclusion\ExclusionStrategyInterface
<?php
namespace JMS\Serializer\Exclusion;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
interface ExclusionStrategyInterface
{
/**
* Whether the class should be skipped.
*
* #param ClassMetadata $metadata
*
* #return boolean
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
/**
* Whether the property should be skipped.
*
* #param PropertyMetadata $property
*
* #return boolean
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
In your case, you can implement your own custom logic in the shouldSkipProperty method and always return false for shouldSkipClass.
Example of implementation can be found in the JMS/Serializer repository
We will reference the created service as acme.my_exclusion_strategy_service below.
In your controller action:
<?php
use Symfony\Component\HttpFoundation\Response;
use JMS\Serializer\SerializationContext;
// ....
$context = SerializationContext::create()
->addExclusionStrategy($this->get('acme.my_exclusion_strategy_service'));
$serial = $this->get('jms_serializer')->serialize($object, 'json', $context);
return new Response($serial, Response::HTTP_OK, array('Content-Type' => 'application/json'));
Or if you are using FOSRestBundle
<?php
use FOS\RestBundle\View;
use JMS\Serializer\SerializationContext;
// ....
$context = SerializationContext::create()
->addExclusionStrategy($this->get('acme.my_exclusion_strategy_service'))
$view = new View($object);
$view->setSerializationContext($context);
// or you can create your own view factory that handles the creation
// of the context for you
return $this->get('fos_rest.view_handler')->handle($view);

As of jms/serializer 1.4.0, the symfony expression language is integrated in its core.
The feature is documented at http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#dynamic-exclusion-strategy and this allows to use runtime exclusion strategies.
An example taken from the documentation is:
class MyObject
{
/**
* #Exclude(if="service('user_manager_service').getSomeRuntimeData(object)")
*/
private $name;
/**
* #Expose(if="service('request_stack').getCurrent().has('foo')")
*/
private $name2;
}
I this example, the services user_manager_service and request_stack are invoked at runtime, and depending on the return (true or false), the property will be exposed or not.
With the same expression language, as of 1.6.0 is possible also to use virtual properties via expression language.
Documented at http://jmsyst.com/libs/serializer/master/reference/annotations#virtualproperty allows to add on the fly data coming from external services

Related

Single position to restrict access to a Doctrine entity

I've just started working with Doctrine and built a simple blog project. One of my requirements is that a blog post should not be visible to anybody (for simpleness, skip an editor's interface) until the publish date is reached.
As far as I see, it's obvious to do so using a custom repository. Let's extend the find method the following way:
public function find($id, $lockMode = null, $lockVersion = null)
{
/** #var Post $post */
$post = parent::find($id, $lockMode, $lockVersion);
if($post->getCreatedAt() > new \DateTime()) {
return null;
}
return $post;
}
This restricts the access for a page showing a single Post entity. For an overview page, the same can be done using a custom method:
public function findForOverview()
{
$query = $this->createQueryBuilder('p')
->where('p.createdAt < CURRENT_TIMESTAMP()')
->orderBy('p.createdAt', 'DESC')
->getQuery();
return $query->getResult();
}
So, even for this simple requirement, I've already written two custom methods. If I continue to work on my project, other restriction limitations might occur and additional ways to load that entity might arise. And as far as I see, for each case I have to implement the logic for all access guards.
Is there no simpler way to do that? I'm thinking of something like an annotation or an "entity load listener" that makes it simple to write one single entry point for all such checks - making it impossible to forget such checks...
Such restrictions are usually implemented by using mechanism of SQL filters in Doctrine. Implementation of this filter works on lower level then DQL and allows you to apply modifications for SQL query being constructed. In your case it may look like this:
namespace App\ORM\Filter;
use App\Entity\Post;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class PostVisibilityFilter extends SQLFilter
{
/**
* Gets the SQL query part to add to a query.
*
* #param ClassMetadata $targetEntity
* #param string $targetTableAlias
* #return string The constraint SQL if there is available, empty string otherwise
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if ($targetEntity->name !== Post::class) {
return '';
}
return sprintf('%s.%s >= now()', $targetTableAlias, $targetEntity->getColumnName('createdAt'));
}
}

How to generate symfony2 translations inside controller?

Symfony2 project. I'm using JMSTranslationsBundle.
Here is a fragment from function inside controller:
if ($user->isAccountConfirmed()) {
$this->toolbar->addInfo('user.account.confirmed');
}
How to generate translation for 'user.account.confirmed' in .xliff file? I mean, what code I should add to this function to be able to translate it?
Looking at the available extraction methods, it explains that there is no automatic extraction for your case available.
You will need to use trans (or any of the other methods explained) in your template or in the controller. Without this hint, the extractor will not be able to find your message. Personally I have used TranslationContainerInterface in one my projects.
With that you simply define a new method in your controller, which returns the "to-be-translated" strings:
<?php
// ...
use JMS\TranslationBundle\Translation\TranslationContainerInterface;
use JMS\TranslationBundle\Model\Message;
class AcmeController extends Controller implements TranslationContainerInterface
{
/**
* {#inheritdoc}
*/
static function getTranslationMessages()
{
return [
Message::create('user.account.confirmed')
];
}
}
An alternate solution would be to directly use the translater service. The call to this service should then again be visible to the extractor. E.g:
/** #var $translator \Symfony\Component\Translation\TranslatorInterface */
$translator = $this->get('translator');
if ($user->isAccountConfirmed()) {
$this->toolbar->addInfo(
$translator->trans('user.account.confirmed')
);
}

How do I implement dynamic (i.e. not cached) Doctrine Asserts in Symfony2?

I have a Doctrine-Entity in my Symfony2-Project, which uses a custom Assert/Constraint to check, if a given date value is before and/or after a given date. This looks like the following simplified code:
In my entity class:
/**
* #var \DateTime
*
* #ORM\Column(name="entry_entered_at", type="date", nullable=true)
* #AppBundleAssert\DateRangeConstraint(max = "today")
*/
private $entryEnteredAt;
The relevant snippet of the corresponding DateRangeConstraint-class:
new \DateTime($this->max)
As you can see, I want to check, if a date is before today. The \DateTime-constructor is able to resolve this to a DateTime-object of today. Nice thing, works fine.
The problem
But it turns out, that Symfony2 caches all those Doctrine-annotations, so today is always resolved to the day, the cache was lastly cleared and my constraint produces nice form errors.
As a workaround for now, I clear the cache on a daily basis, but I need a better solution.
The question
So the question is, what would you suggest, how to implement such a dynamic assert/constraint in Symfony2?
I could implement the constraint inside the form, but it should be in the domain of the entity.
Edit:
I posted as answer and marked it as solution.
The solution and some answers
It turned out, that the built in Range validator is also able to validate a date-range. So I don't need my custom validator at all.
Digging a bit deeper into the built in Range constraint and the base Constraint class gives the reason, why the built in validators can use dynamic parameters like today, but not my incorrect implemented custom validator. The Constraint base class has a __sleep() method that just stores the object vars and its current values on serialization. Thus, when we don't reinitialize the object with a custom __wakeup() method, which would be a false workaround, we only get the cached parameters.
So besides the fact, that the builtin Range constraint already solves my problem, I simply should have done my dynamic new \DateTime($constraint->max) stuff inside the custom DateRangeConstraintValidator and not the cached custom DateRangeConstraint. Just have a look into Symfony\Component\Validator\Constraints\Range and Symfony\Component\Validator\Constraints\RangeValidator to see this in action.
Lessons learned
Your custom Constraint class will be serialized and cached and thus shouldn't do any dynamic things. Just validate the options and define the messages and stuff. Your dynamic validation things (and especially the initialization of dynamic parameters) must be done within your custom ConstraintValidator class.
I suggest you to look at Custom validator, especially Class Constraint Validator.
I won't copy paste the whole code, just the parts which you will have to change.
Extends the Constraint class.
src/Acme/DemoBundle/Validator/Constraints/CheckEntryEnteredAt.php
<?php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class CheckEntryEnteredAt extends Constraint
{
public $message = 'Your error message.';
public function validatedBy()
{
return 'CheckEntryEnteredAtValidator';
}
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
Define the validator by extending the ConstraintValidator class, entryEnteredAt is the field you want to check:
src/Acme/DemoBundle/Validator/Constraints/CheckEntryEnteredAtValidator.php
namespace Acme\DemoBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CheckEntryEnteredAtValidator extends ConstraintValidator
{
public function validate($entity, Constraint $constraint)
{
$today = new \Datetime('today'); // = midnight
if ($entity->entryEnteredAt < $today) {
$this->context->addViolationAt('entryEnteredAt',
$constraint->message, array(), null);
}
}
}
Use the validator:
src/Acme/DemoBundle/Resources/config/validation.yml
Acme\DemoBundle\Entity\AcmeEntity:
constraints:
- Acme\DemoBundle\Validator\Constraints\CheckEntryEnteredAt: ~
(adapted from a previous answer)
public function __construct()
{
$this->entryEnteredAt = new \DateTime();
}
is something like that a solution for your use case? (on new YourEntity() you'll have a today date set for the entryEnteredAt property)
You could also use LifecycleCallbacks, here is an exemple with preUpdate (there is some more, like PrePersist):
on top of your class entity:
* #ORM\HasLifecycleCallbacks()
and
/**
* Set updatedAt
*
* #ORM\PreUpdate
*/
public function setUpdatedAt()
{
$this->updatedAt = new \DateTime();
}

Can I implement my own Symfony2 annotations easily?

Is there anything in the Symfony annotations modules that allow me to use them for other uses?
I know for #Route and #Method you need to extend existing libraries, so its just not that easy i'm guessing.
Currently, i'm working with the JS History API, and would LOVE to put the popState data for my JS files in the annotations. So they are already available when the routing generates the URL.
Q Doesn't this makes sense to have a, HTML5 annotated title, or some attribute here? It would be great to have the ability to define this data, as annotated, right next to the already existing route name and stuff.
Q Is there anybody that has tweaked with the annotations before?
I wanted to clarify my intentions here as I think I left out some crucial details (the mention of History API) for understanding my use case.
There is a few SPA front ends that have been integrated through a front-end bundle, and this connected via AJAX calls to a backend bundle which was a straight RESTful API, with the addition of a very fun-to-develop PHP API class I made that intereprets and processes (routes) the AJAX in a fashion that directly executes other PHP class controller `methods.
I use a lot of ajax for this Symfony 2 app (fosjsrouter) to handle routing. So instead of URLs triggering the routes and actions, the SPA click event fires off AJAX to the back end router, with a large JSON payload, not limited to PHP control parameter's (class/method/var names), and data sets.
OK, so getting back on track; Given the above scenario; In the JS class object end of the router, inside this I thought it was the best place to add some JS History API functionality to it, (state, back button, etc.)
The above class can be called if a history flag was called, which could become responsible for assigning initial state data. Primarily, this is because the JSON data object that's being around in this JS method contains already a lot of the crucial route data, and param information for that route needed in the backend PHP, which comes from the annotations.
So the idea is if I add accessibility for a history state title and URL to the annotations, then I will have access to that information right there available to define the initial state, if flagged, right inside the an ajax.done(), inside this main JS routing method.
Now we can pass state back and forth two ways between the db and realtime client-side async. You can use an observer, or anything, from there front-end, and jobs/queues on the backend to keep it fully reactive. (use React too :-))
EDIT I'm not so sure that's what I was thinking, it looks like its making me set the values of the title and url for this inside the return statement of the PHP function, where I want it set in the annotation (see return 'Matthias Noback';)
So I'm trying this, but where do I set these titles at?
<?php
namespace Blah\CoreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
/**
* #Annotation
*/
class HistoryAnnotationController
{
//history state params are out properties here..
/**
* #var
*/
private $url;
/**
* #var
*/
private $title;
/**
*
*/
public function __construct()
{
}
/**
* #return mixed
*/
public function getTitle()
{
return $this->title;
}
/**
* #return mixed
*/
public function getUrl()
{
return $this->url;
}
}
I want to set it WAY back here, so the ajax that calls this route has access to it.. (look for #historyApiTitle in this code, etc..)
<?php
namespace Blah\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller,
Symfony\Component\HttpFoundation\JsonResponse,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Method,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Route,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Template,
Blah\Bundle\Entity\Test,
Doctrine\ORM\Query; //for hydration
class StuffController
{
/**
* #Route("/some/route/name/{test}", name="some_route_name", options={"expose"=true})
* #param $test
* #return mixed
* #historyApiTitle('This is the get something page')
* #historyApiUrl('/get_something')
*/
public function getSomethingAction($test)
{
$em = $this->getDoctrine()->getManager();
$dql = "
SELECT s
FROM BlahBundle:Stuff s
WHERE s.test = :test";
$query = $em->createQuery($dql);
$query->setParameter('test', $test);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate($query,
$this->get('request')->query->get('page', 1), 1000);
return $this->render('BlahBundle:Stuff:get_something.html.twig', array('pagination' => $pagination));
}
}
Q So looking at these TWO code examples, how do I connect the dots between the two to get this to work?
Yes you can annotations classes you can follow the following tutorial Creating Custom annotations Classes
Basic rules are the follows:
Your class should have the #Annotation -phpdoc comment
/**
* #Annotation
*/
class CustomAnnotation
{
public function __construct($options) {}
}
In Your Needed class just use it in standard way;
class Person
{
/**
* #CustomAnnotation("option")
*/
public function getName()
{
return 'some stuff';
}
}
You should looks at the AOPBundle, it allows you to do treatement from your personnals annotations. But I don't thinks trying to do annotations in the view is a good idea. You need to parse the javascript with php, and it sounds bad.

Symfony 2: Static function [JMSTranslation]

I'm currently working on a project that the previous developer integrated with JMSTranslationBundle.
At this moment, I've did some modification to the application, one of them was to turn the menu to be highly dynamic. (Basically, the user logic of the application have 3 layers and each layer have their own menu).
The menu are stored into the database and accessible through the doctrine entity. To display the label, I store into the DB the "label code" which is used by JMSTranslationBundle as a key to identify it. The desc is by default empty until setted into the translation file. (editable with the _trans route).
Into the documentation of JMS, it is mentionned that one can implement TranslationContainerInterface so when the compilation of the translation file (who are XLIFF file currently) are done, each class implementing this will be called to return a list of Message objects. Here's my issue:
The function to implement is static, meaning that when call, my model Menu (who handle to logic of fetching throught Doctrine repo) is not loaded via the service manager. This means that I do not receive the repository object (since it's loaded by service and pass through the controller):
public function __construct(MenuRepository $objMenuRepo)...
The definition of the function I implements is:
static function getTranslationMessages(){ ... }
My question is: how can I obtain the doctrine (either manager or repository) within that static function . (Since this will be only called on translation initial generation and not by the site itsef, performance is not an issue I worry about).
Also: If anyone have better alternative to propose (that wouldn't involved getting rid of this translation bundle, trust me, it would take quite an amount of time right now), I'm opened to hear them.
Thank you :-)
If some of you are interested, I had to use an alternative solution.
Although it doesn't answer the question on how to use a service within a static context, it will help those who ran into the same issue I had when attempting to implement with JMSTranslation.
To implement the solution (to extract translation key from the database), I had to use the JMS\TranslationBundle\Translation\ExtractorInterface.
I have implement it under this format:
class TranslationRepositoriesExtractor implements ExtractorInterface{
//Loaded through the service container
public function __construct(EntityRepository $objRepositoryNeeded);
// Implementation of the interface ExtractorInterface.
// Within this function, I've used the EntityRepository received in the
// constructor to fetch the list of keys that would be use for translating
/**
* #return \JMS\TranslationBundle\Model\Message[]
*/
public function extract()
}
As you can notice, the extract function return an array of \JMS\TranslationBundle\Model\Message.
After implementing this function, you have to add your object as a service and make it recognizable by JMSTranslationBundle as an extractor. To do so:
<!-- Replace the id of the service, the class path, the id of the argument and the alias
named you want by the value you need in your application -->
<service id="idOrYourService" class="Path\Of\Class\TranslationRepositoriesExtractor">
<argument type="service" id="repository.needed" />
<tag name="jms_translation.extractor" alias="NameOfAlias" />
</service>
The alias tag is used within JMSTranslationBundle to recognize your class as an extractor.
Finally, when generating the files, I had to had to enable the extractor. This can be done via the config, but in my case, was done manually through the command line
php app/console translation:extract --enable-extractor=NameOfAlias en
// NameOfAlias is the same name as the one defined in the tag of your service
I hope I didn't forget any step (if so, feel free to reply in a comment and I'll update the answer).
Happy coding :-)
Using this input, I ended up coding this version of the extractor.
<?php
namespace MyBundle\Service;
use Doctrine\ORM\EntityManager;
use JMS\TranslationBundle\Model\Message;
use JMS\TranslationBundle\Model\MessageCatalogue;
use JMS\TranslationBundle\Translation\ExtractorInterface;
/**
* Extracts translatable strings from Doctrine entities
*
* #package MyBundle\Service
*/
class EntityTranslationExtractor implements ExtractorInterface
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* EntityTranslationExtractor constructor.
*
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #return MessageCatalogue
*/
public function extract()
{
$messageCatalogue = new MessageCatalogue();
// Sample portion of the extraction
$translatableEntities = $this->entityManager->getRepository('MyBundle:MyEntity')->findAll();
foreach ($translatableEntities as $entity) {
$message = new Message($entity::class .'.'. $entity->getName(). '.name');
$message->setDesc(ucwords($entity->getName()));
$messageCatalogue->add($message);
}
return $messageCatalogue;
}
}

Resources