There is a standard feature in sonata-admin-bundle to export data using exporter; But how to make export current entity AND mapped ManyToOne entity with it?
Basically what I want, is to download exactly same data as defined in ListFields.
UPD: In docs, there is only todo
UPD2: I've found one solution, but I do not think it is the best one:
/**
* Add some fields from mapped entities; the simplest way;
* #return array
*/
public function getExportFields() {
$fieldsArray = $this->getModelManager()->getExportFields($this->getClass());
//here we add some magic :)
$fieldsArray[] = 'user.superData';
$fieldsArray[] = 'user.megaData';
return $fieldsArray;
}
I created own source iterator inherited from DoctrineORMQuerySourceIterator.
If value in method getValue is array or instance of Traversable i call method getValue recursive to get value for each "Many" entity:
protected function getValue($value)
{
//if value is array or collection, creates string
if (is_array($value) or $value instanceof \Traversable) {
$result = [];
foreach ($value as $item) {
$result[] = $this->getValue($item);
}
$value = implode(',', $result);
//formated datetime output
} elseif ($value instanceof \DateTime) {
$value = $this->dateFormater->format($value);
} elseif (is_object($value)) {
$value = (string) $value;
}
return $value;
}
In your admin class you must override method getDataSourceIterator to return your own iterator.
This
$this->getModelManager()->getExportFields($this->getClass());
returns all entity items. Better practice is to create explicit list of exported items in method getExportFields()
public function getExportFields()
{
return [
$this->getTranslator()->trans('item1_label_text') => 'entityItem1',
$this->getTranslator()->trans('item2_label_text') => 'entityItem2.subItem',
//subItem after dot is specific value from related entity
....
Key in array is used for export table headers (here is traslated).
Related
I manage to get a filtered collection of my Note entities with API Platform, using the #ApiFilter(SearchFilter::class) annotation.
Now I want to convert the json response which is an hydra collection
Example :
{
"#context": "/api/contexts/Note",
"#id": "/api/notes",
"#type": "hydra:Collection",
"hydra:member": []
}
to an archive containing one file by Note and return its metadata.
Example :
{
"name": "my_archive.zip",
"size": 12000,
"nb_of_notes": 15
}
I want to keep the SearchFilter benefits. Is the Normalization the good way to go ?
How to declare the normalizer ? How to access the collection/array of Notes in my normalize() method ?
According to the documentation symfony custom_normalizer , you can create a custom normalizer for your Note entity (for example NoteNormalizer). In the supportsNormalization method your must precise that the normalizer will only affect your Note entity by providing Note entity class. So in the normalize method, you will get each item of your ArrayCollection of Note. If you want to be sure, you can make a dump to $data variable (dd($data)) inside this normalize method, and you will have the first element of you ArrayCollection.
that's how I tried to understand it.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class NoteNormalizer implements ContextAwareNormalizerInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer) // Like in documentation you can inject here some customer service or symfony service
{
$this->normalizer = $normalizer;
}
public function normalize($topic, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($topic, $format, $context);
$data['name'] = 'some name';
$data['size'] = 12000;
$data['nb_of_notes'] = 15;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Note;
}
}
Or if you want you can use this command to generate it automatically :
php bin/console make:serializer:normalizer
And give the name : NoteNormalizer
Simply create a "collection Normalizer" :
note: works the same for vanilla symfony projects too.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
class NoteCollectionNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function supportsNormalization($data, $format = null, array $context = []): bool
{
if(!is_array($data) || (!current($data) instanceof Note)) {
return false;
}
return true;
}
/**
* #param Note[] $collection
*/
public function normalize($collection, $format = null, array $context = [])
{
// ...
}
}
I am working on blog project where is lots of posts from different authors and i want to create a simple filter that would return me only posts by given author:
This is my controller that takes data from user:
/**
* #Route("/author/{author}", name="post_author")
* #Template()
*/
public function findByAuthorAction($author)
{
$criteria = /*this is what i need*/
$posts=$this->get('cvut_fit_biwt1_blog')->findPostBy($criteria);
return array(
'posts'=>$posts
);
}
This is how findPostBy looks like:
public function findPostBy(array $criteria)
{
return $this->postRepository->findBy($criteria);
}
And finally implementation of findBy in Doctrine EntityRepository:
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->loadAll($criteria, $orderBy, $limit, $offset);
}
Can you tell me how to build criteria array in my contoller so it would filter my posts (if it is even possible) ?
Assuming the {author} parameter is the author's URI name or something (this is purely speculative), you have to fetch either the author object or the author ID from Doctrine first:
$authorObj = $this->getDoctrine()->getManager()
->getRepository('AcmeBundle:Author')->findOneByName(urldecode($author));
Then pass the ID or Author object into the criteria using an associative array:
$criteria = array('author' => $authorObj);
If $author is in fact the ID of the author, then you can just do:
$criteria = array('author' => $author);
Note that you will get a Collection of objects even if you just get one result. You should use findOneBy:
public function findPostBy(array $criteria)
{
return $this->postRepository->findOneBy($criteria);
}
Just getting started with Symfony, so please bear with me here.
I have an Entity with a field "myField" that is stored in the database as an ENUM with values 'Y' or 'N'. (This is an old DB schema that I'm working with and trying to use symfony as an app to manipulate the data).
I want to represent "myField" with a checkbox for on or off (0 or 1). When the form is saved, how would I transform the value to the appropriate 'Y' or 'N' value before persisting it to the database?
I looked at Data Transformers, and that may be where I need to go, but it seems so silly to create a new class and file just to convert that data. This is a very simple transformation.
Maybe instead I would just change the setMyField($myField) () {} setter method on the Entity to convert the data there? And likewise, convert it back in getMyField() {}? But doesn't Doctrine also use the setter methods? If so, then when Doctrine set a value, it would be the proper value (0 or 1) from the database and wouldn't need transforming. But if the form set the value, it would need transforming.
Here's the action code:
public function newAction(Request $request)
{
$course = new Course();
$form = $this->createForm(new CourseForm(), $course);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($course);
$em->flush();
return $this->redirect($this->generateUrl('courses'));
}
return $this->render('PucsTestBundle:Course:new.html.twig', array(
'form' => $form->createView(),
));
}
I think I could convert the data myself in the controller just before I call handleRequest, but that is probably not good practice.
Additionally, I have a field in the database "days" that is just a VARCHAR that would be something like MTW for Monday, Tuesday Wednesday. I want to present a checkbox for all these values, and then post-process the form submission to convert the data to the appropriate single value.
You should use a custom Doctrine Type as defined here in the documentation
<?php
namespace My\Project\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* My custom datatype.
*/
class OldBooleanType extends Type
{
const NAME = 'OldBoolean'; // modify to match your type name
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'OldBoolean';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value === 'Y'){
return true;
}
if ($value === 'N'){
return false;
}
return null;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value === true){
return 'Y';
}
if ($value === false){
return 'N';
}
return null;
}
public function getName()
{
return self::NAME;
}
}
From Symfony documentation: http://symfony.com/doc/current/cookbook/form/data_transformers.html
Data transformers are used to translate the data for a field into a format that can be displayed in a form (and back on submit).
I have extended the collection form field type with CollectionTableExtension. I am trying to set a variable, FormView::$vars['headers'] so that collection can be output with default headers when is_table is true and no user-defined headers are available. The default headers should be the same as the labels which would normally be applied to collection members.
e.g.
if FooType has 3 fields, foo, bar and fibble, its labels will be Foo, Bar and Fibble (after humanizing with Twig template) or the values stored in the label attribute of each property.
So if my DoofusType has a collection of FooTypes,
$builder->add('foos', 'collection', array('type'=>'acc_foo', 'is_table'=>true));
should result in a view where headings for the collection will be Foo, Bar and Fibble or the values of the label attributes of the FooType if they are set.
Here is my collection extension:
<?php
namespace ACC\MainBundle\Form;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CollectionTableExtension extends AbstractTypeExtension
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('is_table'));
$resolver->setOptional(array('headers'));
$resolver->setOptional(array('caption'));
}
public function getExtendedType()
{
return 'collection';
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('is_table', $options)) {
$view->vars['is_table'] = $options['is_table'];
if (array_key_exists('caption', $options)) $view->vars['caption'] = $options['caption'];
if (array_key_exists('headers', $options)) {
$view->vars['headers'] = $options['headers'];
}else{
//harvest labels from collection members, but HOW?
}
}
}
}
The issue is that I don't know how to access the properties of the collection element type from inside the collection extension. I could access the first element in the collection like this:
if($form->has('0')) {
foreach($form->get('0')->all() as $item)
$view->vars['headers'][] = $item->getName();
}
but that doesn't help if the collection is empty. And it doesn't help if the collection element type has defined labels. Any ideas?
After much slogging through Symfony source code, I've found a way to do it. Essentially, in ::buildView a dummy collection element of the correct type must be instantiated so that its labels or properties can be extracted. Here's what I did:
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (array_key_exists('is_table', $options)) {
$view->vars['is_table'] = $options['is_table'];
if (array_key_exists('caption', $options)) $view->vars['caption'] = $options['caption'];
if (array_key_exists('headers', $options)) {
if($options['headers']===false){ //no headers
unset($view->vars['headers']);
}else if (is_array($options['headers'])){ //headers passed in
$view->vars['headers'] = $options['headers'];
}
}else { //harvest labels from collection elements
$elementtype = $form->getConfig()->getOption('type');
//should be a guard clause here so types that won't supply good headers (e.g. a collection of text fields) will skip the rest
$element = $form->getConfig()->getFormFactory()->create($elementtype); //get dummy instance of collection element
$fields = $element->all();
$headers = array();
foreach($fields as $field){
$label= $field->getConfig()->getOption('label');
$headers[] = empty($label) ? $field->getName() : $label;
}
$view->vars['headers'] = $headers;
}
}
}
If anyone has a cleaner method, I'm all ears.
//Suppose Entity Notes has property 'creationdate' & 'getCreationDate()' method to access.
DefaultController extends Controller {
public function indexAction(){
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('Bundle:Notes');
$notes = $repository->findBy(array('userid' => $userId);
//Now I want to sort the notes array as per creation date using usort
usort($notes, array($this,"cmp"));
}
function cmp($a, $b) {
return strtotime($a->getCreationDate()) > strtotime($b->getCreationDate())? -1:1;
}
}
You can set the order in your call to the repository rather than after like so...
$notes = $repository->findBy(
array('userid' => $userId), // search criteria
array('creationdate' => 'ASC') // order criteria
);
I know you said you wanted to use usort but it seems kind of unnecessary.