symfony doctrine2 store entity as json - symfony

I have an entity with several properties that could be grouped together, are crucial for it's usage and I won't ever have to index them.
I thought about storing them in separate class, just for the order.
I'd like to store the object of this class inside a text column in a json representation.
I tried something like this:
class Market
{
/**
* #var Fee
* #ORM\Column(type="json")
*/
protected Fee $fee;
(...)
}
and the Fee class:
class Fee implements Serializable
{
private float $taker;
private float $maker;
public function __construct(float $taker, float $maker)
{
$this->taker = $taker;
$this->maker = $maker;
}
/**
* #return float
*/
public function getTaker(): float
{
return $this->taker;
}
/**
* #return float
*/
public function getMaker(): float
{
return $this->maker;
}
public function serialize()
{
return json_encode(
[
'taker' => $this->getTaker(),
'maker' => $this->getMaker(),
]
);
}
public function unserialize($serialized)
{
$decoded = json_decode($serialized, true);
$this->taker = $decoded['taker'];
$this->maker = $decoded['maker'];
}
}
I'd like to have something like this in final column "fee":
{
"taker": 0.0016,
"maker": 0.004
}
but instead I always get an empty json {}.
Please advise how to do what I intend to.
I'm using Symfony 5.3.6 with PHP 8.0.9

You used a JSON field type, which expects an array instead of a string. So you shouldn't serialize it before setting.
You can use a object type instead:
class Market
{
/**
* #ORM\Column(type="object")
*/
protected Fee $fee;
}
(I also removed #var Fee because it's superfluous)

Related

Applying a correct logic after successful validation in Symfony

First of I all, I created the whole example below specifically for this question because the actual example is very big so if it looks stupid then assume that it is not for now!
I'm trying to come up with a solution so that I can call a correct private method (bankA() or bankB()) in controller if the validation successfully passes. As you can see in the custom validation constraint, I only check the $bank->code property however the condition is not actually that simple (there is repository checks so on) - (as I said above, it is trimmed down version). So, could please someone tell me, how will I know that which private method I should call in controller after successful validation? I'm happy to create dedicated validators if necessary so open for suggestions and examples.
Note: I looked into symfony group validation documentation but didn't really get the picture how I could apply to my scenario.
EXAMPLE REQUEST
{ "id": 66, "code": "A" }
{ "id": 34, "code": "B" }
CONTROLLER
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* #Route("bank", service="application_frontend.controller.bank")
*/
class BankController extends Controller
{
private $validator;
public function __construct(
ValidatorInterface $validator
) {
$this->validator = $validator;
}
/**
* #param Request $request
*
* #Route("")
* #Method({"POST"})
*
* #throws Exception
*/
public function indexAction(Request $request)
{
$content = $request->getContent();
$content = json_decode($content, true);
$bank = new Bank();
$bank->id = $content['id'];
$bank->code = $content['code'];
$errors = $this->validator->validate($bank);
if (count($errors)) {
throw new Exception($errors[0]->getMessage());
}
// OK, validation has passed so which one do I call now ?!?!
$this->bankA($bank);
$this->bankB($bank);
}
private function bankA(Bank $bank)
{
// Do something nice with Bank
}
private function bankB(Bank $bank)
{
// Do something bad with Bank
}
}
BANK MODEL
use Application\FrontendBundle\Validator\Constraint as BankAssert;
/**
* #BankAssert\Bank
*/
class Bank
{
/**
* #var int
*/
public $id;
/**
* #var string
*/
public $code;
}
CUSTOM VALIDATOR
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Bank extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return get_class($this).'Validator';
}
}
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class BankValidator extends ConstraintValidator
{
public function validate($bank, Constraint $constraint)
{
if ($bank->code == 'A') {
return;
}
if ($bank->code == 'B') {
return;
}
$this->context->buildViolation('Bank error')->addViolation();
}
}
Depending on how many codes there are you could either do...
if ('A' === $bank->getCode()) {
$this->bankA($bank);
} else {
$this->bankB($bank);
}
Or..
$method = 'bank'.$bank->getCode();
if (!method_exists($this, $method)) {
throw new \Exception('Method "'.$method.'" does not exist');
}
$this->$method();
All of that being said, it would be advisable to move all of this work into a dedicated service rather than in your controller. Then in your controller use something like...
$this->container->get('do_something_to_bank.service')->processAction($bank);

Add dynamic property on entity to be serialized

I have this REST API. Whenever request comes to get a resource by id ( /resource/{id}) I want to add a permissions array on that object on the fly (entity itself does not have that field).
What I came up with is this event listener. It checks the result the controller has returned:
class PermissionFinderListener {
...
public function onKernelView(GetResponseForControllerResultEvent $event) {
$object = $event->getControllerResult();
if (!is_object($object) || !$this->isSupportedClass($object)) {
return;
}
$permissions = $this->permissionFinder->getPermissions($object);
$object->permissions = $permissions;
$event->setControllerResult($object);
}
....
}
The problem is that the JMS Serializer opts out this dynamic property on serialization. I tried making the onPostSerialize event subscriber on JMS serializer, but then there are no clear way to check if this is a GET ONE or GET COLLECTION request. I don't need this behaviour on GET COLLECTION and also it results a huge performance hit on collection serialization. Also I don't want to create any base entity class with permission property.
Maybe there is some other way to deal with this scenario?
What I could imagine is a combination of Virtual Property and Serialization Group:
Add a property to your entity like:
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"}) */
*
* #return string
*/
public function getPermissions()
{
return $permissionFinder->getPermissions($this);
}
Only thing you need to do then is to serialize 'includePermissions' group only in your special case (see http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies)
If you don't have access to $permissionFinder from your entity you could as well set the permission attribute of an entity from a Controller/Service before serializing it.
EDIT:
This is a bit more code to demonstrate what I mean by wrapping your entity and using VirtualProperty together with SerializationGroups. This code is not tested at all - it's basically a manually copied and stripped version of what we're using. So please use it just as an idea!
1) Create something like a wrapping class for your entity:
<?php
namespace Acquaim\ArcticBundle\Api;
use JMS\Serializer\Annotation as JMS;
/**
* Class MyEntityApi
*
* #package My\Package\Api
*/
class MyEntityApi
{
/**
* The entity which is wrapped
*
* #var MyEntity
* #JMS\Include()
*/
protected $entity;
protected $permissions;
/**
* #param MyEntity $entity
* #param Permission[] $permissions
*/
public function __construct(
MyEntity $entity,
$permissions = null)
{
$this->entity = $entity;
$this->permissions = $permissions;
}
/**
* #Serializer\VirtualProperty
* #Serializer\SerializedName("permissions")
* #Serializer\Groups({"includePermissions"})
*
* #return string
*/
public function getPermissions()
{
if ($this->permissions !== null && count($this->permissions) > 0) {
return $this->permissions;
} else {
return null;
}
}
/**
* #return object
*/
public function getEntity()
{
return $this->entity;
}
}
2) In your controller don't return your original Entity, but get your permissions and create your wrapped class with entity and permissions.
Set your Serialization Context to include permissions and let the ViewHandler return your serialized object.
If you don't set Serialization Context to includePermissions it will be excluded from the serialized result.
YourController:
$myEntity = new Entity();
$permissions = $this->get('permission_service')->getPermissions();
$context = SerializationContext::create()->setGroups(array('includePermissions'));
$myEntityApi = new MyEntityApi($myEntity,$permissions);
$view = $this->view($myEntityApi, 200);
$view->setSerializationContext($context);
return $this->handleView($view);

Symfony2 Form with custom FormType calls DataTransformer with same data in both directions

I have made a new FormType and it extends the entity type via
//...
public function getParent()
{
return 'entity';
}
Which lead my edit form to complain that an integer was not My/Entity/Type and I need a data transformer. So I created one. This is the abbreviated version (it's just the basic tutorial version)
//...
public function reverseTransform($val)
{
// Entity to int
return $val->getId();
}
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
//...
Then added it to my form type
//...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new IdToMyModelTransformer($this->em));
}
This fixed me viewing my form, but now when I submit the form with an entity picked from my custom widget it tries to call transform not reverseTransform with the $val as an int the ->getId() fails on a non-object.
I can't figure out the correct way of doing this. If I use 'choice' as my widget parent I get a different set of issues (choice default constraints triggered saying it is invalid data?)
I need an entity passed to my widget so it can extract the meta data for display, but I can't post an entity back of course. How do I tell the form that?
Tried setting 'data_class' => null but no joy. Checking network tab shows the value is sent correctly when posting the form.
Update 1
So I re-read the DataTransformer page and that diagram got me thinking, especially after rubber-duck programming above, I ask the form for Entity but expect it to receive ints.. so I actually need a unidirectional transformer, ViewTransformer -> Get entity for display, get posted an int from widget, don't transform it just pass straight through. Which works and I just get the "invalid data" error on update.
Now I have in my Transformer:
public function transform($val)
{
// Int to Entity
return $repo->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Update 2
That seems to have fixed it now, although for some reason if I post int 2 in my form the string "2/" is sent to my transformer. Any ideas on that? FOr now I'm cleaning the string in transformer, but seems like it just shouldnt be happening.
By what I'm seeing in your transformer class you're not implementing the code right. This should be the correct implementation:
namespace App\YourBundle\Form\DataTransformer;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class IdToMyModelTransformer implements DataTransformerInterface
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em) {
$this->em = $em;
}
/**
* Transforms a value from the original representation to a transformed representation.
*
* This method is called on two occasions inside a form field:
*
* 1. When the form field is initialized with the data attached from the datasource (object or array).
* 2. When data from a request is submitted using {#link Form::submit()} to transform the new input data
* back into the renderable format. For example if you have a date field and submit '2009-10-10'
* you might accept this value because its easily parsed, but the transformer still writes back
* "2009/10/10" onto the form field (for further displaying or other purposes).
*
* This method must be able to deal with empty values. Usually this will
* be NULL, but depending on your implementation other empty values are
* possible as well (such as empty strings). The reasoning behind this is
* that value transformers must be chainable. If the transform() method
* of the first value transformer outputs NULL, the second value transformer
* must be able to process that value.
*
* By convention, transform() should return an empty string if NULL is
* passed.
*
* #param mixed $object The value in the original representation
*
* #return mixed The value in the transformed representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function transform($object) {
if (null === $object) {
return null;
}
return $object->getId();
}
/**
* Transforms a value from the transformed representation to its original
* representation.
*
* This method is called when {#link Form::submit()} is called to transform the requests tainted data
* into an acceptable format for your data processing/model layer.
*
* This method must be able to deal with empty values. Usually this will
* be an empty string, but depending on your implementation other empty
* values are possible as well (such as empty strings). The reasoning behind
* this is that value transformers must be chainable. If the
* reverseTransform() method of the first value transformer outputs an
* empty string, the second value transformer must be able to process that
* value.
*
* By convention, reverseTransform() should return NULL if an empty string
* is passed.
*
* #param mixed $categoryId The value in the transformed representation
*
* #return mixed The value in the original representation
*
* #throws TransformationFailedException When the transformation fails.
*/
public function reverseTransform($id) {
if (!$id || $id <= 0) {
return null;
}
if(!ctype_digit($id)){
throw new TransformationFailedException();
}
$repo = $this->em->getRepository('...');
$result = $repo->findOneBy(array('id' => $id));
if (null === $result) {
throw new TransformationFailedException(
sprintf(
'Entity with id does not exist!',
$id
)
);
}
return $result;
}
}
In the IdToMyIntType you would have something like this:
namespace App\YourBundle\Form\Type;
use App\YourBundle\Form\DataTransformer\IdToMyModelTransformer ;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class IdToMyModelType extends AbstractType {
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct( EntityManager $em ) {
$this->em = $em;
}
public function buildForm( FormBuilderInterface $builder, array $options ) {
$transformer = new IdToMyModelTransformer ( $this->em );
$builder->addModelTransformer( $transformer );
}
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array('invalid_message' => 'Something went wrong message.'));
}
public function getParent() {
return 'entity';
}
public function getName() {
return 'id_to_model_type';
}
}
I would suggest you check out the DataTransformerInterface and read the documentation over the methods. It'll briefly explain what is that method expected to do. Also, in case you have problems implementing it, you can always check the official documentation, which contains a working example and build up from there.
As per my last update I realised, because I was only using my form data to display the currently saved entity relation (the rest is provided by ajax) and not in the same format the form would be receiving it in it lead to some confusion.
To follow the tutorials wording:
Model data
This was all to remain as-is (No model datatransformer needed)
Norm data
No changes
View data (unidirection transformation required)
Transform()
ID to Entity so widget can access other properties
ReverseTransform()
Posted ID is in correct format so we just return it
Code
Very simplified:
private $om;
public function __construct (ObjectManager om)
{
$this->om = $om;
}
public function transform($val)
{
// Int to Entity
return $om->getRepository('MyBundle:EntityName')->findOneBy($val);
}
public function reverseTransform($val)
{
// Do nothing
return $val;
}
Hopefully that helps anyone else who lets their requirements confuse them!

Symfony - FOSRestBundle - show selected fields

I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
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?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio

Symfony money field type and doctrine

What is your strategy to store monetary values with Doctrine? The Symfony's money field is quite handy but how to map this to Doctrine's column? Is there a bundle for this that provides DBAL type?
float or int column types are insufficient because when you deal with money you often deal with currency too. I'm using two fields for this but it's awkward to handle manually.
Consider using the decimal type:
/**
* #ORM\Column(type="decimal", precision=7, scale=2)
*/
protected $price = 0;
Note that there are currencies which have three decimal positions. If you intend to use such currencies, the scale parameter should be 3. If you intend to mix currencies with two and three decimal positions, add a trailing 0 if there are only two decimal positions.
Attention: $price will be a string in PHP. You can either cast it to float or multiply it with 100 (or 1000, in the case of currencies with three decimal positions) and cast it to int.
The currency itself is a separate field; it can be a string with the three letter currency code. Or – the clean way – you can create a table with all currencies you’re using and then create a ManyToOne relation for the currency entry.
I recommend using a value object like Money\Money.
# app/Resources/Money/doctrine/Money.orm.yml
Money\Money:
type: embeddable
fields:
amount:
type: integer
embedded:
currency:
class: Money\Currency
# app/Resources/Money/doctrine/Currency.orm.yml
Money\Currency:
type: embeddable
fields:
code:
type: string
length: 3
# app/config.yml
doctrine:
orm:
mappings:
Money:
type: yml
dir: "%kernel.root_dir%/../app/Resources/Money/doctrine"
prefix: Money
class YourEntity
{
/**
* #ORM\Embedded(class="\Money\Money")
*/
private $value;
public function __construct(string $currencyCode)
{
$this->value = new \Money\Money(0, new \Money\Currency($currencyCode));
}
public function getValue(): \Money\Money
{
return $this->value;
}
}
You could define a own field type as long as you tell the doctrine how to handle this. To explain this I made up a ''shop'' and ''order'' where a ''money''-ValueObject gets used.
To begin we need an Entity and another ValueObject, which gets used in the entity:
Order.php:
<?php
namespace Shop\Entity;
/**
* #Entity
*/
class Order
{
/**
* #Column(type="money")
*
* #var \Shop\ValueObject\Money
*/
private $money;
/**
* ... other variables get defined here
*/
/**
* #param \Shop\ValueObject\Money $money
*/
public function setMoney(\Shop\ValueObject\Money $money)
{
$this->money = $money;
}
/**
* #return \Shop\ValueObject\Money
*/
public function getMoney()
{
return $this->money;
}
/**
* ... other getters and setters are coming here ...
*/
}
Money.php:
<?php
namespace Shop\ValueObject;
class Money
{
/**
* #param float $value
* #param string $currency
*/
public function __construct($value, $currency)
{
$this->value = $value;
$this->currency = $currency;
}
/**
* #return float
*/
public function getValue()
{
return $this->value;
}
/**
* #return string
*/
public function getCurrency()
{
return $this->currency;
}
}
So far nothing special. The "magic" comes in here:
MoneyType.php:
<?php
namespace Shop\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Shop\ValueObject\Money;
class MoneyType extends Type
{
const MONEY = 'money';
public function getName()
{
return self::MONEY;
}
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'MONEY';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
list($value, $currency) = sscanf($value, 'MONEY(%f %d)');
return new Money($value, $currency);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof Money) {
$value = sprintf('MONEY(%F %D)', $value->getValue(), $value->getCurrency());
}
return $value;
}
public function canRequireSQLConversion()
{
return true;
}
public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('AsText(%s)', $sqlExpr);
}
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('PointFromText(%s)', $sqlExpr);
}
}
Then you can use the following code:
// preparing everything for example getting the EntityManager...
// Store a Location object
use Shop\Entity\Order;
use Shop\ValueObject\Money;
$order = new Order();
// set whatever needed
$order->setMoney(new Money(99.95, 'EUR'));
// other setters get called here.
$em->persist($order);
$em->flush();
$em->clear();
You could write a mapper which maps your input coming from Symfony's money field into a Money-ValueObject to simplify this further.
A couple more details are explained here: http://doctrine-orm.readthedocs.org/en/latest/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html
Untested, but I used this concept before and it worked. Let me know if you got questions.
I were serching for a solution to this problem and googling I landed on this page.
There, there is illustrated the Embeddable field available since Doctrine 2.5.
With something like this you can manage values as monetary ones that have more "params".
An example:
/** #Entity */
class Order
{
/** #Id */
private $id;
/** #Embedded(class = "Money") */
private $money;
}
/** #Embeddable */
class Money
{
/** #Column(type = "int") */ // better than decimal see the mathiasverraes/money documentation
private $amount;
/** #Column(type = "string") */
private $currency;
}
Hope this will help.
UPDATE
I wrote a PHP library that contains some useful value objects.
There is also a value object to manage monetary values (that wraps the great MoneyPHP library) and persist them to the database using a Doctrine type.
This type saves the value to the database in the form of 100-EUR that stands for 1 Euro.

Resources