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.
Related
Let's say I have a main form (foo entity form) where I have an embedded form (bar entity embedded form).
Let's also say that foo - 1/many - bar (of course).
Now, I want to display all possible bar entities in the system, even if they aren't associated with foo. So before bind form with foo entity, I usually do some query, extract data and, if bar isn't already associated with foo, associate it (basically i create some "virtual" association that haven't to be persisted under certain circumstance. I can't use symfony2 native method as I need to handle some attributes and Symfony2 don't let me do that)
All works like a charm. Now I added to bar form a non-mapped field that should help me to know whenever to save or not the association.
Into controller I check for the presence of this field and if not, I artificially unset the index of the collection from request object. When I dump the request all is good (embedded elements without flag aren't there anymore).
BUT
When I bind request object to entity, all embedded form elements are still there. This is driving me totally cray.
Code example
(I will not paste entity code as the issue is not there. I will not paste form code also)
public function createAction()
{
$foo = new Foo();
$foo_form = $this->createForm(new FooType(), $foo);
if ($request->getMethod() == 'POST') {
$parameter_array = $request->request->all();
$bar_array = $parameter_array['foo']['bar'];
//If I dump here, of course, all bar are setted
foreach ($bar_array as $index => $bar) {
if (!isset($bar['associate'])) { //this is the flag
unset($parameter_array['foo']['bar'][$index]);
}
}
$request->request->replace($parameter_array);
//If i dump $request->request->all(); all non-flagged bar are gone
$foo_form->bind($request);
$foo->getBars(); //If I dump this all bar(s) are still there (even the not-flagged ones)
}
}
I've found a workaround. As I can't controller - or at least it seems I cannot - directly parameter bag when entities are involved, I've simply act upon object after form and object are binded.
My code is now this
public function createAction()
{
$foo = new Foo();
$foo_form = $this->createForm(new FooType(), $foo);
if ($request->getMethod() == 'POST') {
$foo_form->bind($request);
if ($foo_form->isValid()) {
$parameter_array = $request->request->all();
if (isset($parameter_array['foo']['bar'])) {
$bars = $foo->getBars();
$bar_array = $parameter_array['foo']['bar'];
foreach ($bar_array as $index => $bar) { //Of course here
if (!isset($bar['associate'])) { // I can use array_filter
$bars->remove($index); // or something similar. Is just more readable that way for this answer
}
}
$foo->setBars($bars);
}
}
}
}
Is there a better solution?
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/
I can use Crypt to encrypt/decrypt my data. I want to encrypt some information in my db (such as the name, email, phone number to name a few).
Assuming that I want EVERYTHING to be encrypted, I want to be able to do this in the background by itself, which I can perform by overwriting the create and save functions:
// For instance, the save() function could become
public function save(array $options = array())
{
foreach ($this->attributes as $key => $value)
{
if (isset($value)) $this->attributes[$key] = Crypt::encrypt($value);
}
return parent::save($options);
}
Now, I want the decryption to be performed the same way, so that when I say User::find($id), the returned $user is already decrypted. Also other functions such as firstOrFail() get() first() and all to work as well.
I also would like this functionality to be extended when I use relationships (so User::with('someOtherTable')->find($id) also work).
Would this be possible? If this is not possible, I am thinking of creating a helper function decyrpt()
function decrypt($array)
{
if (!is_array($array)) return Crypt::decrypt($array);
$result = [];
foreach($array as $key => $value) $result[$key] = decrypt($value);
return $result;
}
And pass all my results through this first, and then start using them, but it would be nicer if Laravel would provide this, or if there was a "Laravel Way" of doing this.
It doesn't really make sense to encrypt everything. For example, you never want to encrypt the primary key; that doesn't even make sense. Likewise you probably don't want to encrypt the date fields; you'll lose the ability to perform any sort of SQL query on them.
With that in mind, you can try something like this:
class BaseModel extends Eloquent {
protected $encrypt = [];
public function setAttribute($key, $value)
{
if (in_array($key, $this->encrypt))
{
$value = Crypt::encrypt($value);
}
return parent::setAttribute($key, $value);
}
public function getAttribute($key)
{
if (in_array($key, $this->encrypt))
{
return Crypt::decrypt($this->attributes[$key]);
}
return parent::getAttribute($key);
}
public function attributesToArray()
{
$attributes = parent::attributesToArray();
foreach ($attributes as $key => $value)
{
if (in_array($key, $this->encrypt))
{
$attributes[$key] = Crypt::decrypt($value);
}
}
return $attributes;
}
}
then have all you models extend this one, and set the $encrypt property to whatever columns you want encrypted for that particular model.
P.S. If you want to use Eloquent's accessor functionality, you'll have to play with this a bit more.
It's worth mentioning Elocrypt library for Laravel 4. It's a more elaborate solution that works the same way. If you're using Laravel 5 use this one instead: Elocrypt 5.
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);
}
}
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.