Change date format in xls export with Sonata Admin Bundle - symfony

I taken over responsibility for a Symfony2 application, built on the Sonata Admin Bundle, and have been asked to make a small change by the users. In the xls export of a list page, the dates all appear as e.g. Wed, 01 Aug 2012 00:00:00 +0200, but the Excel format is General. The users would like the data in this column to be an Excel date type, so that it is sort-able.
I have been able to find some information about export customization, but this mostly concerns choosing the list export file types, or which fields to include, rather than how to change the format in the exported document. A similar question was asked here (I think) but there is no answer.
I think this would (or should) be very simple, but it is certainly not obvious. Any help would be much appreciated.

A small improvement for Marciano's answer.
Makes the code a bit more resilient against sonata updates.
public function getDataSourceIterator()
{
$datasourceit = parent::getDataSourceIterator();
$datasourceit->setDateTimeFormat('d/m/Y'); //change this to suit your needs
return $datasourceit;
}

In my admin class EmployeeAdmin I use getExportFields function specifies which fields we want to export:
public function getExportFields() {
return array(
$this->trans('list.label_interview_date') => 'interviewDateFormatted'
);
}
interviewDateFormatted is actually a call to the corresponding entity (Employee) method getInterviewDateFormatted which looks like this:
public function getInterviewDateFormatted() {
return ($this->interviewDate instanceof \DateTime) ? $this->interviewDate->format("Y-m-d") : "";
}
This way I can change date format or do other necessary changes to the fields I want to export.

this is my code. It's work!
use Exporter\Source\DoctrineORMQuerySourceIterator;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
and function:
/**
* {#inheritdoc}
*/
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
$fields=$this->getExportFields();
$query = $datagrid->getQuery();
$query->select('DISTINCT ' . $query->getRootAlias());
$query->setFirstResult(null);
$query->setMaxResults(null);
if ($query instanceof ProxyQueryInterface) {
$query->addOrderBy($query->getSortBy(), $query->getSortOrder());
$query = $query->getQuery();
}
return new DoctrineORMQuerySourceIterator($query, $fields,'d.m.Y');
}

just add this in your admin (overriding a method of the admin class you are extending). Found it reading the code. It's not in the docs.
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
$datasourceit = $this->getModelManager()->getDataSourceIterator($datagrid, $this->getExportFields());
$datasourceit->setDateTimeFormat('d/m/Y'); //change this to suit your needs
return $datasourceit;
}

Did you managed to make it work?
Date format is defined as parameter for new DoctrineORMQuerySourceIterator.php (https://github.com/sonata-project/exporter/blob/master/lib/Exporter/Source/DoctrineORMQuerySourceIterator.php)
DoctrineORMQuerySourceIterator.php is created inside getDataSourceIterator function (https://github.com/sonata-project/SonataDoctrineORMAdminBundle/blob/2705f193d6a441b9140fef0996ca392887130ec0/Model/ModelManager.php)
Inside of Admin.php there is function calling it:
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
return $this->getModelManager()->getDataSourceIterator($datagrid, $this->getExportFields());
}
If you write your own getDataSourceIterator() then you can change date format.

Since sonata-admin 4.0, the function getDataSourceIterator() is tagged as final, so you can't override it.
So you need to create a decorating iterator :
<?php
namespace App\Service\Admin;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Exporter\DataSourceInterface;
use Sonata\DoctrineORMAdminBundle\Exporter\DataSource;
use Sonata\Exporter\Source\DoctrineORMQuerySourceIterator;
use Sonata\Exporter\Source\SourceIteratorInterface;
class DecoratingDataSource implements DataSourceInterface
{
private DataSource $dataSource;
public function __construct(DataSource $dataSource)
{
$this->dataSource = $dataSource;
}
public function createIterator(ProxyQueryInterface $query, array $fields): SourceIteratorInterface
{
/** #var DoctrineORMQuerySourceIterator $iterator */
$iterator = $this->dataSource->createIterator($query, $fields);
$iterator->setDateTimeFormat('Y-m-d H:i:s');
return $iterator;
}
}
And add it in your config/services.yaml
services:
...
App\Service\Admin\DecoratingDataSource:
decorates: 'sonata.admin.data_source.orm'
arguments: ['#App\Services\Admin\DecoratingDataSource.inner']
Found here : https://docs.sonata-project.org/projects/SonataDoctrineORMAdminBundle/en/4.x/reference/data_source/

Related

Symfony Options Resolver allow Invalid Options

I am using the optionsResolver component in a silex project to resolve options for configuration. If I don't explicitly set options with setRequired, setOptional, or setDefaults I get an error Fatal error: Uncaught exception 'Symfony\Component\OptionsResolver\Exception\InvalidOptionsException' with message 'The option "option.key" does not exist. Known options are: ...
I want to allow options that are not defined with those methods. I tried to use my own class that extends the class but the class uses to many private methods that would require me to copy/paste most of the class.
Is there a better way to do this?
I use this component in ApiGen a I think you can't add options that aren't specified.
If you know all options, it is the best practice to name them all.
What is your specific use case?
I solved this by creating two resolvers. One with the fixed option list, the other is where I add options dynamically. I then split the incoming options array into two arrays using array_filter:
$dynamicOptions = array_filter($options, function($k) use ($fixedOptionKeys) {
if (!in_array($k, $fixedOptionKeys)) {
return true;
}
}, ARRAY_FILTER_USE_KEY);
$fixedOptions = array_filter($options, function($k) use ($fixedOptionKeys) {
if (in_array($k, $fixedOptionKeys)) {
return true;
}
}, ARRAY_FILTER_USE_KEY);
I think this solution will be more pretty and simplier.
Just create your own optionsResolver that extends the symfony base one and override the 'resolve' method
Hope it will help
use Symfony\Component\OptionsResolver\OptionsResolver;
class ExtraOptionsResolver extends OptionsResolver
{
/**
* Strip options that have been passed to
* this method to be resolved, and that have not been defined as default or required options
* The default behaviour is to throw an UndefinedOptionsException
*
* #author Seif
*/
public function resolve(array $options = array())
{
// passing by ref in loops is discouraged, we'll make a copy
$transformedInputOptions = $options;
foreach ($options as $key => $option) {
if (!in_array($key, $this->getDefinedOptions())) { // option was not defined
unset($transformedInputOptions[$key]); // we will eject it from options list
}
}
return parent::resolve($transformedInputOptions);
}
}

Is there a way to access the symfony2 container within an SQLFilter?

is there any possibility to get the service-container of symfony2 within an SQLFilter or can i maybe directly use a service as SQLFilter?
I know that this isn't a "clean" way, but i have to perform several checks directly before the final submit of the query gets fired (as i have to append conditions to the WHERE-statement, i can't use lifecycle-events at this point).
it's not clean but you could try this:
<?php
class MyBundle extends Bundle
{
public function boot()
{
$em = $this->container->get('doctrine.orm.default_entity_manager');
$conf = $em->getConfiguration();
$conf->addFilter(
'test',
'Doctrine\Filter\TestFilter'
);
$em->getFilters()->enable('test')->setContainer($this->container);
}
}

Symfony Write to sfGuard Table

I know it's not ideal, but I have a few extra fields on the sfGuard User table and I would like to write to it from another module. There is a specific field that is a simple integer but I would like it to -1 each time they perform a specific task. Here is what I tried. I don't get an error message but it also doesn't write the number to the table.
public function executePublish(sfWebRequest $request)
{
$this->user = $this->getUser()->getGuardUser();
$times = ($this->user->getTimes() - 1);
$this->getUser()->getGuardUser()->setTimes($times);
}
This is the "Publish" action for a different module. Am I doing this wrong? Thanks.
You didn't maked an INSERT to the database by setTimes($times)
you have to call save() method after setting all values of object.
It should look like this:
public function executePublish(sfWebRequest $request)
{
$this->user = $this->getUser()->getGuardUser();
$times = ($this->user->getTimes() - 1);
$this->user->setTimes($times);
$this->user->save();
}

Right form events to display modified data and update modified data?

A simple task: before displaying the form, if $data->getRole() starts with "ROLE_", remove this string and display only the rest. When user submit the form, do the opposite: add "ROLE_" before the name.
What's the best place to do this? Actually i'm using PRE_SET_DATA and POST_BIND. Are these the right events to perform this operation?
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function(DataEvent $event){
if(is_null($data = $event->getData()) || !$data->getId()) return;
$data->setRole(strtoupper(preg_replace('/^ROLE_/i', '',
$data->getRole())));
});
$builder->addEventListener(FormEvents::POST_BIND,
function(DataEvent $event) {
if(is_null($data = $event->getData()) || !$data->getId()) return;
$data->setRole('ROLE_' . strtoupper($data->getRole()));
});
Well reading the role without the prefix "ROLE" is not something I would do using events. As they obsfusicate your workflow, events should be used with care! Working with symfony for some time, I used them once or twice when there was really no other way. All the other times there was a better way.
I would tend to simply add a function getShortRole and setShortRole and use shortRole within your Entity:
class MyEntity {
private $role;
public function setShortRole($role) {
$this->role = 'ROLE_' . strtoupper($role);
}
public function getShortRole() {
return strtoupper(preg_replace('/^ROLE_/i', '', $this->role));
}
}
You are saving yourself a lot of trouble working with models instead of events!
A second, more complicated way would be to use a Model which represents the form instead of the Entity and maps the form to the entity. Here is a good article about this here!
I use it myself and it works nice.

Virtual fields in Symfony2

I am still thinking about the best way to work with tags in Symfony. I did look at FPNTagBundle, but I didn't find an easy way to work this into the CRUD forms.
I also found http://xoxco.com/clickable/jquery-tags-input which would give a perfect widget. As it in- and outputs comma separated strings, I thought I could just define a virtual field in my model, that displays the tag object array as such a list.
public function addTag(\Wein\StoreBundle\Entity\Tag $tag)
{
$this->tag[] = $tag;
$this->makeTagFieldFromTags();
}
public function setTagField($tagField)
{
$this->tagField = $tagField;
$this->makeTagsFromTagField();
}
public function makeTagsFromTagField()
{
$tags=explode(',', $this->tagField);
$tagObjects=array();
$em = $this->getDoctrine()->getEntityManager();
foreach($tags as $tag) {
$tag=trim($tag);
$tagObject = **???**;
$tagObjects[]=$tagObject;
}
$this->tag=$tagObjects;
}
public function makeTagFieldFromTags()
{
$tags=array();
foreach($this->tag as $tag) {
$tags[]=$tag->__toString();
}
$this->tagField = implode(',', $tags);
}
The I could just use a form element on this field. Unfortunatly, I don't see a way to translate the strings into Tag-objects insite the entity, as I don't have access to the entity manager.
So what is the clean way?
The clean way is to use a data transformer. It transforms the strings into your tag entities at the "form" side and not in the entity, so you can keep your entity clean.

Resources