I'm currently trying to save my dates in database with de UTC timezone.
To get the user timezone, I have a JS function that makes an AJAX request to my back-end like this :
import $ from 'jquery';
import jstz from 'jstz';
export default function setSessionTimezone(route)
{
var timezone = jstz.determine();
$.ajax({
type:'POST', async:true, cache:false, url:route, data:"timezone="+timezone.name(),
success:function(data) { if (data.reloadPage) location.reload(); }
});
}
This method is called only if the timezone is not already in session.
So, for now, I have the user timezone in my back-end, that was the first step.
I want to save it in the database.
With this SO post, I found something interesting : Symfony buildForm convert datetime to local and back to utc
They recommend to use the "model_timezone" and "view_timezone" for the forms, so did I :
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UtcTypeExtension extends AbstractTypeExtension
{
/**
* #var SessionInterface
*/
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
/**
* Return the class of the type being extended.
*/
public static function getExtendedTypes(): iterable
{
return [TimeType::class, DateType::class];
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'model_timezone' => 'UTC',
"view_timezone" => $this->session->get('tools_timezone')
]);
}
}
And hooora, it works.
But only within the forms.
If I want to display the dates with Twig or from PHP, I need to get the timezone from session and change the DateTime's Timezone.
So I searched another option.
I found this on the Doctrine Website to change the timezone directly from Doctrine.
This sound interesting, but I'm probably missing a point because it doesn't seem to work, even after I added the following configuration :
doctrine:
dbal:
types:
datetime: SomeNamespace\DoctrineExtensions\DBAL\Types\UTCDateTimeType
So I would like to know if what I want to do is even possible ? Or if I'm forced to override Twig "date" filter to use my timezone ? And if I want to display a date from PHP, I'm also force to use the timezone from the session?
I found something that seem to answer my question.
I added a listener to the doctrine's event "postLoad".
For information, the TimezoneProvider just return the DateTimezone object from the session or UTC if it was not defined.
use DateTime;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping\Column;
use SomeNamespace\Provider\TimezoneProvider;
use ReflectionClass;
use ReflectionException;
use Symfony\Component\PropertyAccess\PropertyAccess;
class DateTimeConverterListener
{
/**
* #var AnnotationReader
*/
private $reader;
/**
* #var TimezoneProvider
*/
private $timezoneProvider;
public function __construct(AnnotationReader $reader, TimezoneProvider $timezoneProvider)
{
$this->reader = $reader;
$this->timezoneProvider = $timezoneProvider;
}
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getObject();
$propertyAccessor = PropertyAccess::createPropertyAccessor();
try {
$reflection = new ReflectionClass(get_class($entity));
//We search the properties where we need to update the Timezone.
foreach ($reflection->getProperties() as $property) {
$annotation = $this->reader->getPropertyAnnotation($property, Column::class);
if (!$annotation instanceof Column)
continue;
switch ($annotation->type) {
case "time":
case "date":
case "datetime":
/** #var DateTime|null $attribute */
$attribute = $propertyAccessor->getValue($entity, $property->getName());
if (null === $attribute)
continue 2;
//The getTimezone function returns UTC in case of no session information. And because it's a
// DateTime object, we don't need to set the value after the modification
$attribute->setTimezone($this->timezoneProvider->getTimezone());
break;
}
}
} catch (ReflectionException $e) {
//Abort the transformation
}
}
}
To display the date properly with the twig filter "|date", I also update Twig through an event :
use SomeNamespace\Provider\TimezoneProvider;
use Twig\Environment;
use Twig\Extension\CoreExtension;
class SetupTwigTimezoneListener
{
/**
* #var TimezoneProvider
*/
private $timezoneProvider;
/**
* #var Environment
*/
private $twig;
public function __construct(TimezoneProvider $timezoneProvider, Environment $twig)
{
$this->timezoneProvider = $timezoneProvider;
$this->twig = $twig;
}
public function onKernelRequest()
{
//Define the timezone of the application based of the timezone of the user
$this->twig->getExtension(CoreExtension::class)->setTimezone($this->timezoneProvider->getTimezone()->getName());
}
}
I'm not quite sure this is a perfect solution, but it seem to work.
Related
i am building an Api with symfony 4.2 and want to use jms-serializer to serialize my data in Json format, after installing it with
composer require jms/serializer-bundle
and when i try to use it this way :
``` demands = $demandRepo->findAll();
return $this->container->get('serializer')->serialize($demands,'json');```
it gives me this errur :
Service "serializer" not found, the container inside "App\Controller\DemandController" is a smaller service locator that only knows about the "doctrine", "http_kernel", "parameter_bag", "request_stack", "router" and "session" services. Try using dependency injection instead.
Finally i found the answer using the Symfony serializer
it's very easy:
first : istall symfony serialzer using the command:
composer require symfony/serializer
second : using the serializerInterface:
.....//
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
// .....
.... //
/**
* #Route("/demand", name="demand")
*/
public function index(SerializerInterface $serializer)
{
$demands = $this->getDoctrine()
->getRepository(Demand::class)
->findAll();
if($demands){
return new JsonResponse(
$serializer->serialize($demands, 'json'),
200,
[],
true
);
}else{
return '["message":"ooooops"]';
}
}
//......
and with it, i don't find any problems with dependencies or DateTime or other problems ;)
As I said in my comment, you could use the default serializer of Symfony and use it injecting it by the constructor.
//...
use Symfony\Component\Serializer\SerializerInterface;
//...
class whatever
{
private $serializer;
public function __constructor(SerializerInterface $serialzer)
{
$this->serializer = $serializer;
}
public function exampleFunction()
{
//...
$data = $this->serializer->serialize($demands, "json");
//...
}
}
Let's say that you have an entity called Foo.php that has id, name and description
And you would like to return only id, and name when consuming a particular API such as foo/summary/ in another situation need to return description as well foo/details
here's serializer is really helpful.
use JMS\Serializer\Annotation as Serializer;
/*
* #Serializer\ExclusionPolicy("all")
*/
class Foo {
/**
* #Serializer\Groups({"summary", "details"})
* #Serializer\Expose()
*/
private $id;
/**
* #Serializer\Groups({"summary"})
* #Serializer\Expose()
*/
private $title;
/**
* #Serializer\Groups({"details"})
* #Serializer\Expose()
*/
private $description;
}
let's use serializer to get data depends on the group
class FooController {
public function summary(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('summary');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
public function details(Foo $foo, SerializerInterface $serialzer)
{
$context = SerializationContext::create()->setGroups('details');
$data = $serialzer->serialize($foo, json, $context);
return new JsonResponse($data);
}
}
I need to improve the application of my work that I’ve done in Symfony2 now that we’re expanding to an international level and we must implement a system of time zones so that that each user can modify the date that they will receive notifications and other alerts. Our time zone of origin is UTC+1 (Europe/Madrid) so we have to save the dates in the database with this time zone. But when it comes to the app, it should be able to show in the settings the time that user configured.
How can I implement it in Symfony 2 so that I won’t have to modify all of the controllers and twig templates?
Can it be done in event listener?
Finally I found a solution, getting your information and searching similar things, specially this --> how to get the type of a doctrine entity property helped me to develop the final code.
Here is what I done:
I've extended the DateTime of Doctrine into a new class UTCDateTimeType:
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\DateTimeType;
class UTCDateTimeType extends DateTimeType {
static private $utc;
static function getUtc(){
return self::$utc;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof \DateTime) {
$value->setTimezone(self::getUtc());
}
return parent::convertToDatabaseValue($value, $platform);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (null === $value || $value instanceof \DateTime) {
return $value;
}
$converted = \DateTime::createFromFormat(
$platform->getDateTimeFormatString(),
$value,
self::$utc ? self::$utc : self::$utc = new \DateTimeZone('Europe/Madrid')
);
if (! $converted) {
throw ConversionException::conversionFailedFormat(
$value,
$this->getName(),
$platform->getDateTimeFormatString()
);
}
return $converted;
}
}
So when get or persit the data of datetime, it's allways in my UTC timezone.
Then, before bootstrapping the ORM, I've overrided the datetime types:
Type::overrideType('datetime', UTCDateTimeType::class);
Type::overrideType('datetimetz', UTCDateTimeType::class);
I edited my User entity to have a time zone field ( PHP time zone identifier)
On a LoginListener -> onSecurityInteractiveLogin, I've injected the Session and when a user log in, I set a "timezone" variable to Session with the user time zone field.
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event){
$user = $event->getAuthenticationToken()->getUser();
$this->session->set("timezone",new \DateTimeZone($user->getTimeZone()));
// ...
}
I made a TimeZoneListener which listen to postLoad doctrine event (triggered when an Entity is fully loaded from DDBB)
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Translation\TranslatorInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\Annotations\AnnotationReader;
class TimeZoneListener {
protected $session;
protected $container;
protected $router;
protected $securityContext;
protected $translator;
protected $docReader;
public function __construct(RouterInterface $router, Session $session, TranslatorInterface $translator, $container){
$this->session = $session;
$this->router = $router;
$this->translator = $translator;
$this->container = $container;
$this->docReader = new AnnotationReader();
}
public function postLoad(LifecycleEventArgs $args){
$reader = $this->docReader;
$entity = $args->getEntity();
$reflect = new \ReflectionClass($entity);
$props = $reflect->getProperties();
foreach($props as $prop){
$docInfos = $reader->getPropertyAnnotations($prop);
foreach($docInfos as $info){
if(!$info instanceof \Doctrine\ORM\Mapping\Column) continue;
if($info->type !== "datetime") continue;
$getDateMethod = 'get'.ucfirst($prop->getName());
$val = $entity->{$getDateMethod}();
if($val){
$val->setTimeZone($this->session->get("timezone") ? $this->session->get("timezone") : new \DateTimeZone("Europe/Madrid"));
}
}
}
}
}
In postLoad method I search for each property of type datetime and then I set to logged user timeZone (previously setted at login into session)
Now, each time a entity is loaded, when a datetime field is reached, at render stage, the datetime offset is sucessfully applied and for each user it displays as spected.
1) Set default timezone, i.e. in event listener that listens kernel.request event.
2) Create event listener that listens security.interactive_login event and there extract user from the event, then get his own timezone settings and apply. (An example)
According to the documentation
If the value passed to the date filter is null, it will return the
current date by default. If an empty string is desired instead of the
current date, use a ternary operator:
http://twig.sensiolabs.org/doc/2.x/filters/date.html
The problem is the solution provided entails that we revisit all dates in the application and apply the ternary operation as we never want to show today's date instead of null.
is it possible to override the default date filter? if so how can I implement this. We're using twigs with symfony 2.7
As explained here in the doc, you can override an existing filter:
To overload an already defined filter, test, operator, global
variable, or function, re-define it in an extension and register it as
late as possible (order matters).
Here is the code to return an empty string instead of the current date if null:
class DateEmptyIfNull extends Twig_Extension
{
public function getFilters()
{
return array(
new Twig_Filter('date', array($this, 'dateFilter')),
);
}
public function dateFilter($timestamp, $format = 'F j, Y H:i')
{
$result = '';
if($timestamp !== null)
{
$result = parent::dateFilter($timestamp, $format);
}
return $result;
}
}
$twig = new Twig_Environment($loader);
$twig->addExtension(new DateEmptyIfNull());
From the documentation:
If the value passed to the date filter is null, it will return the current date by default. If an empty string is desired instead of the current date, use a ternary operator:
{{ post.published_at is empty ? "" : post.published_at|date("m/d/Y") }}
You can check it at https://twig.symfony.com/doc/3.x/filters/date.html
Here's the Twig 3.0 solution
Extension class:
namespace Application\Twig\Extensions;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class DateWithFallback extends AbstractExtension
{
/**
* #var Environment
*/
protected $twig;
/**
* DateWithFallback constructor.
*
* #param Environment $twig
*/
public function __construct(Environment $twig)
{
$this->twig = $twig;
}
/**
* #return array|TwigFilter[]
*/
public function getFilters(): array
{
return [
new TwigFilter('date', [$this, 'dateFilter']),
];
}
/**
* #param string|null $timestamp
* #param string $fallback
* #param string $format
* #return string
*/
public function dateFilter(?string $timestamp, string $fallback = 'Not set', string $format = 'd/m/Y'): string
{
if ($timestamp !== null) {
return twig_date_format_filter($this->twig, $timestamp, $format);
}
return $fallback;
}
}
Adding the extension assuming that $this->twig is your Twig Environment:
$this->twig->addExtension(new DateWithFallback($this->twig));
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.
I use the sonata-admin bundle.
I have the relationship with the user (FOSUserBundle) in the PageEntity.
I want to save the current user which create or change a page.
My guess is get the user object in postUpdate and postPersist methods of the admin class and this object transmit in setUser method.
But how to realize this?
On the google's group I saw
public function setSecurityContext($securityContext) {
$this->securityContext = $securityContext;
}
public function getSecurityContext() {
return $this->securityContext;
}
public function prePersist($article) {
$user = $this->getSecurityContext()->getToken()->getUser();
$appunto->setOperatore($user->getUsername());
}
but this doesn't work
In the admin class you can get the current logged in user like this:
$this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser()
EDIT based on feedback
And you are doing it this? Because this should work.
/**
* {#inheritdoc}
*/
public function prePersist($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
/**
* {#inheritdoc}
*/
public function preUpdate($object)
{
$user = $this->getConfigurationPool()->getContainer()->get('security.token_storage')->getToken()->getUser();
$object->setUser($user);
}
Starting with symfony 2.8, you should use security.token_storage instead of security.context to retrieve the user. Use constructor injection to get it in your admin:
public function __construct(
$code,
$class,
$baseControllerName,
TokenStorageInterface $tokenStorage
) {
parent::__construct($code, $class, $baseControllerName);
$this->tokenStorage = $tokenStorage;
}
admin.yml :
arguments:
- ~
- Your\Entity
- ~
- '#security.token_storage'
then use $this->tokenStorage->getToken()->getUser() to get the current user.
I was dealing with this issue on the version 5.3.10 of symfony and 4.2 of sonata. The answer from greg0ire was really helpful, also this info from symfony docs, here is my approach:
In my case I was trying to set a custom query based on a property from User.
// ...
use Symfony\Component\Security\Core\Security;
final class YourClassAdmin extends from AbstractAdmin {
// ...
private $security;
public function __construct($code, $class, $baseControllerName, Security $security)
{
parent::__construct($code, $class, $baseControllerName);
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
// customize the query used to generate the list
protected function configureQuery(ProxyQueryInterface $query): ProxyQueryInterface
{
$query = parent::configureQuery($query);
$rootAlias = current($query->getRootAliases());
// ..
$user = $this->security->getUser();
// ...
return $query;
}
}