I have a page that has a form that generates a query that renders relevant information from my database:
Render Example
The controller function I use creates the form using Symfony/Doctrine and then calls on a repository function to query the database based on the fields the user has supplied. I then display these results in a table in a twig file for the user to examine.
My issue is I'm fairly new to Symfony and am having an issue then creating an 'export' button where I can somehow grab the form's data that was passed in so I can re-run the repository function in a new route, OR pass the data from the previous query itself to a function in a new route in the controller where I can then turn this data into a CSV file.
I have it working in what feels like a very poor way currently, by adding hidden input fields in the twig file and then grabbing the values from each field and putting together objects in a controller function that I then turn into a CVS.
I'm looking for suggestions on a clean way to get either form data or query data from my exportAction function/route in my ServiceController into a new exportServiceAction function/route (or any other advised methods to accomplish the same goal)
Controller Function
/**
* Creates a form for Exporting Services associated with techs
* #Route("/export", name="service_export", methods={"GET", "POST"})
*/
public function exportActions(Request $request, EntityManagerInterface $em)
{
$staffEntities = $em->getRepository('AppBundle:User')->buildFindByRole('ROLE_STAFF')->getQuery()->getResult();
$staffOptions = [];
foreach ($staffEntities as $staff) {
$staffUsername = $staff->getUsername();
if(!array_key_exists($staffUsername, $staffOptions)) {
$staffOptions[$staffUsername] = $staffUsername;
}
}
$campusEntities = $em->getRepository('AppBundle:Campus')->findBy([], ['name' => 'ASC']);
$campusOptions = [];
foreach ($campusEntities as $campus) {
$campusName = $campus->getName();
if (!array_key_exists($campusName, $campusOptions)) {
$campusOptions[$campusName] = $campusName;
}
}
$buildingEntities = $em->getRepository('AppBundle:Building')->findBy([], ['name' => 'ASc']);
$buildingOptions = [];
foreach ($buildingEntities as $building) {
$buildingName = $building->getName();
if (!array_key_exists($buildingName, $buildingOptions)) {
$buildingOptions[$buildingName] = $buildingName;
}
}
$roomEntities = $em->getRepository('AppBundle:Room')->findBy([], ['name' => 'ASC']);
$roomOptions = [];
foreach ($roomEntities as $room) {
$roomName = $room->getName();
if (!array_key_exists($roomName, $roomOptions)) {
$roomOptions[$roomName] = $roomName;
}
}
$options = [
'techs' => $staffOptions,
'campuses' => $campusOptions,
'buildings' => $buildingOptions,
'rooms' => $roomOptions,
];
$searchForm = $this->createForm('AppBundle\Form\SearchServiceType', [], $options);
$searchForm->handleRequest($request);
if($searchForm->isSubmitted() && $searchForm->isValid()) {
$data = $searchForm->getData();
dump($data);
$queriedAssignments = $em->getRepository('AppBundle:Service')->findForExport($data);
if (empty($queriedAssignments)) {
$queriedAssignments = ["errorMessage" => "No Results Found!"];
}
}
return $this->render('service/export.html.twig', [
'form' => $searchForm->createView(),
'assignments' => $queriedAssignments ?? null,
]);
}
(This is where i'd like to access either the data or queriedAssignments variables to create a CSV)
Repository Function
public function findForExport(array $params = [])
{
$em = $this->getEntityManager();
$qb = $em->createQueryBuilder();
$qb->select('a', 'u', 'bg', 'bsi', 'e', 's')
->from('AppBundle:Assignment', 'a')
->leftJoin('a.staff', 'u')
->leftJoin('a.task', 't')
->leftJoin('a.billingGroup', 'bg')
->leftJoin('bg.billingServiceItems', 'bsi')
->leftJoin('a.eventInstance', 'e')
->leftJoin('e.serviceRequest', 's')
->groupBy('a');
$qb->orderBy('a.date', 'ASC')
->addOrderBy('a.startTime', 'ASC')
->addOrderBy('a.endTime', 'ASC');
if (isset($params['startDate'])) {
$qb->andWhere('a.date >= :startDate')
->setParameter('startDate', $params['startDate']);
}
if (isset($params['endDate'])) {
$qb->andWhere('a.date <= :endDate')
->setParameter('endDate', $params['endDate']);
}
if ($params['promptStaff']) {
if ($this->paramIsNotEmpty($params['techs'])) {
$qb->andWhere('u.username IN (:techs)');
$qb->setParameter('techs', $params['techs']);
$qb->orderBy('u.username', 'ASC')
->addOrderBy('a.date', 'ASC');
}
}
if ([$params['promptLocation']]) {
if ($this->paramIsNotEmpty($params['campusName']) || $this->paramIsNotEmpty($params['buildingName']) || $this->paramIsNotEmpty($params['roomName'])) {
$qb->join('s.location', 'l');
}
}
if ($this->paramIsNotEmpty($params['campusName'])) {
$qb->andWhere('l.campusName IN (:campusName)');
$qb->setParameter('campusName', $params['campusName']);
}
if ($this->paramIsNotEmpty($params['buildingName'])) {
$qb->andWhere('l.buildingName IN (:buildingName)');
$qb->setParameter('buildingName', $params['buildingName']);
}
if($this->paramIsNotEmpty($params['roomName'])) {
$qb->andWhere('l.roomName IN (:roomName)');
$qb->setParameter('roomName', $params['roomName']);
}
if ($this->paramIsNotEmpty($params['campusName']) || $this->paramIsNotEmpty($params['buildingName']) || $this->paramIsNotEmpty($params['roomName'])) {
$qb->orderBy('l.campusName', 'ASC')
->addOrderBy('l.buildingName', 'ASC')
->addOrderBy('l.roomName', 'ASC')
->addOrderBy('a.date', 'ASC')
->addOrderBy('a.startTime', 'ASC')
->addOrderBy('a.endTime', 'ASC');
}
if ($this->paramIsNotEmpty($params['searchVenue'])) {
$qb->leftJoin('s.location', 'l');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->like('l.venueName', ':searchVenue'),
$qb->expr()->like('l.venueAddress', ':searchVenue'),
$qb->expr()->like('l.venueCity', ':searchVenue'),
$qb->expr()->like('l.venueState', ':searchVenue'),
$qb->expr()->like('l.venueZipCode', ':searchVenue')
)
);
$qb->setParameter('searchVenue', '%'.$params['searchVenue'].'%');
$qb->orderBy('l.venueState', 'ASC')
->addOrderBy('l.venueCity', 'ASC')
->addOrderBy('l.venueName', 'ASC')
->addOrderBy('a.date', 'ASC')
->addOrderBy('a.startTime', 'ASC')
->addOrderBy('a.endTime', 'ASC');
}
if ($this->paramIsNotEmpty($params['startTime'])) {
$qb->andWhere('a.startTime >= :startTime');
$qb->setParameter('startTime', $params['startTime']);
}
if ($this->paramIsNotEmpty($params['endTime'])) {
$qb->andWhere('a.endTime <= :endTime');
$qb->setParameter('endTime', $params['endTime']);
}
return $qb->getQuery()->getResult();
}
protected function paramIsNotEmpty($param)
{
return $param instanceof ArrayCollection ? !$param->isEmpty() : !empty($param);
}
Form
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('startDate', DateTimeType::class, [
'widget' => 'single_text',
'required' => false,
'html5' => false,
'attr' => ['class' => 'date', 'placeholder' => 'e.g. mm/dd/yyyy', 'autocomplete' => 'off'],
'format' => 'MM/dd/yyyy',
])
->add('endDate', DateTimeType::class, [
'widget' => 'single_text',
'required' => false,
'html5' => false,
'attr' => ['class' => 'date', 'placeholder' => 'e.g. mm/dd/yyyy', 'autocomplete' => 'off'],
'format' => 'MM/dd/yyyy',
])
->add('searchVenue', SearchType::class, [
'required' => false,
'label' => 'Venue Search (Name, Address, City, State, Zip)',
'attr' => ['placeholder' => 'Press Enter to Search']
])
->add('promptStaff', CheckboxType::class, [
'required' => false,
'label' => 'Filter by Staff'
])
->add('promptLocation', CheckboxType::class, [
'required' => false,
'label' => 'Filter by Location'
])
->add('promptTime', CheckboxType::class, [
'required' => false,
'label' => 'Filter by Time'
])
->add('promptVenue', CheckboxType::class, [
'required' => false,
'label' => 'Search Venues'
])
->add('techs', ChoiceType::class, array(
'required' => false,
'expanded' => true,
'multiple' => true,
'label' => 'Staff Username',
'choices' => $options['techs']
))
->add('campusName', ChoiceType::class, array(
'required' => false,
'expanded' => true,
'multiple' => true,
'choices' => $options['campuses']
))
->add('buildingName', ChoiceType::class, array(
'required' => false,
'expanded' => true,
'multiple' => true,
'choices' => $options['buildings']
))
->add('roomName', ChoiceType::class, array(
'required' => false,
'expanded' => true,
'multiple' => true,
'choices' => $options['rooms']
))
->add('startTime', ChoiceType::class, array(
'choices' => $times,
'required' => false,
'placeholder' => 'Please Select'
))
->add('endTime', ChoiceType::class, array(
'choices' => $times,
'required' => false,
'placeholder' => 'Please Select'
));
}
Twig
{% block body %}
<h1>Staff Time Export</h1>
{{ form_start(form) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
<div class="filter-prompts">
{{ form_row(form.promptStaff) }}
{{ form_row(form.promptLocation) }}
{{ form_row(form.promptTime) }}
</div>
<div class="staff-select">
{{ form_row(form.techs) }}
</div>
<div class="location-select">
{{ form_row(form.promptVenue)}}
<div class="location-on-campus">
{# dropdowns #}
{{ form_row(form.campusName) }}
{{ form_row(form.buildingName) }}
{{ form_row(form.roomName) }}
</div>
<div class="location-off-campus">
{{ form_row(form.searchVenue) }}
</div>
</div>
<div class="time-select">
{{ form_row(form.startTime)}}
{{ form_row(form.endTime)}}
</div>
<div class="button-container">
<button class="button button--grey" type="submit">Search</button>
</div>
{{ form_end(form) }}
{% if assignments is not null and assignments is not empty and assignments.errorMessage is not defined %}
<form action="{{ path('service_export_do')}}" method="post">
<table class="table--dashboard">
<thead>
<tr>
<th>Task Description</th>
<th>Staff Name</th>
<th>Username</th>
<th>Date</th>
<th>Start Time</th>
<th>End Time</th>
<th>Hours</th>
<th>Unit Cost</th>
</tr>
</thead>
<tbody>
{% for assignment in assignments %}
<tr>
<td><input type="hidden" name="serviceIDs[]" value="{{ assignment.id }}"></input>{{ assignment.task.description ?? 'n/a'}}</td>
<td>{{ assignment.staff.lastname }}, {{ assignment.staff.firstname }}</td>
<td> {{ assignment.staff.username }}</td>
<td> {{ assignment.date|date("m/d/Y") }}</td>
<td> {{ assignment.startTime|date("H:i:s") }}</td>
<td> {{ assignment.endTime|date("H:i:s") }}</td>
<td> {% for billingServiceItem in assignment.billingServiceItems %}
{% if billingServiceItem.hours %}{% endif %}
{{ billingServiceItem.hours }}
{% else %}
0
{% endfor %}</td>
<td> {% for billingServiceItem in assignment.billingServiceItems %}
${{ billingServiceItem.unitCost }}
{% endfor %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="button-container">
<button class="button button--grey" name="export_all" type="submit">Export</button>
</div>
</form>
{% elseif (assignments.errorMessage is defined)%}
<h1>
{{ assignments['errorMessage'] }}
</h1>
{% endif %}{% endblock %}
(Here I left out all but one of the hidden input types i was using previously for legibility)
Some things I've tried based off google searches (maybe not very well):
Passing the data to the route via a route parameter:Symfony Docs on Route Parameters
Creating a private variable in the controller for the data , then accessing it via getter/setters (Yes dumb, I was desperate)
Trying to move the export button into the same form as the query and rendering it conditionally, then adding a href path to a new controller route
Somehow trying to re-render the form again and re-querying it using the same information and then turning that data into the CSV
I feel like I'm missing a very simple way to add a function and/or route here that uses the same data that the user used to query and display the table, to create a csv. Thank you in advance for any suggestions and sorry if this is is hard to understand.
EDIT: What I ended up doing
So after trying some of these helpful suggestions, what I found to be the easiest solution for me at my experience level was to simply grab the ID from each entry from my query via a hidden input tag. In my controller I created a new route for creating a cvs and grabbed the IDs using $request->request->get('IdsFromEntries'); I then created a new function in my repository that joined related tables I needed for the fields I wanted that were related to these entries. I was then able to loop through this query data and assign each field to a column for my cvs. Doing this I was able to keep my front end the same and have the user simply click the export button at the button of my search results table to download the cvs containing the table's data.
The simplest way would be to add some optional suffix in your current route like .csv. If it is set then get all data that you need and create CSV with it.
This way you don't need to create new controller and extract your duplicating code... Just add one condition and return rendered page in one case and CSV file in another.
I had a similiar issue to be resolved. I created two routes.
One route showed the user interface including the form, a table of data and an export button. The second route was justed used to export the data. Both routes were connected to the same form. Because the selection of the user could be shared publicly without leaking any secrets, the attached form used the http verb "GET" effectivly attaching the users selection as a query string parameter to the view route. When rendering the export button, I concatenated the export routes path with the query string.
This way I was able to separate both actions into two routes and share content between them, no cookies or js needed.
I want form data updated after ajax save. Cause if item was new (id - empty), it tries to create new one each time. Also there are back-end generated fields which are appears after save.
<?php $form = ActiveForm::begin([
'method' => 'post',
'action' => ['category/save', 'id' => $category ? $category->id : ''],
'enableClientValidation' => true,
// 'enableAjaxValidation' => false,
'validateOnChange' => false,
'validateOnBlur' => false,
'validateOnSubmit' => true,
'options' => [
'id' => 'customer-update',
'class' => 'ajax-submit',
],
'fieldConfig' => [
'template' => '<div class="row-content-col1">{label}</div><div class="row-content-col2">{input}{error}</div>'
]
]); ?>
.......
<?php echo $form->field($category, 'url')->textInput(['class' => 'ip', 'readonly' => true]); ?>
......
<?php $form->end(); ?>
Form field produce such html:
<div class="row-content-col1"><label class="control-label" for="category-url">Url</label></div><div class="row-content-col2"><input type="text" id="category-url" class="ip" name="Category[url]" readonly><div class="help-block"></div></div>
</div>
And than on controller i return this (tried different variations):
{"error":false,"message":"Category 'asdfzsdf sdf' saved","data":{"name":"asdfzsdf sdf","url":"asdfzsdf-sdf","project_id":1,"id":21}}
What is valid response for ajax form? Or is there other way to handle this all ?
Pjax is really useful for your challenge, Just add your form inside of Pjax widget. add form action to new path(such: site/control-data).
In your action method do what you want, but send response like that :
return $this->renderAjax('form path',$model);
It's the general of what you must do.
But maybe you have problem with jquery or pjax or need some more data, but all questions have an answer,
See Pjax for ActiveForm
How adding a required form row with twig
i have used this code but not work when i submit it ignore it
{{ form_row(form.prenom,{'attr': {'required': 'required'} }) }}
Controller
$candidat = new Candidat();
$form = $this->createForm(CandidatType::class, $candidat);
;
$this->render("cvtheque/ajouter_candidat.html.twig",['form' => $form->createView()])
You should specify it on the building of your form :
->add('prenom', TextType::class, [
'label' => 'customer.edit.firstname',
'required' => true
])
I'm using a partial view to display page header, that view accepts two variables one is Icon the other is a title.
pageHeader.blade.php:
<div class="page-header">
<div class="row">
<!-- Page header, center on small screens -->
<h1 class="col-xs-12 col-sm-4 text-center text-left-sm"><i class="fa {{$icon}} page-header-icon"></i> {{$title}}</h1>
</div>
and I'm using it like so:
#include('zdashboard._partials.pageHeader',['icon'=>'fa-pencil','title'=>'<strong>Editing</strong>'.$center->translations()->whereLang('en')->first()->name])
Sometimes I like to make one word strong or italic like the example above but, blade engine won't render the HTML tags I'm typing as part of title variable (the output like the photo down).
So is there any idea how to solve this? Am I doing it!
wrong?
The output
By default in Laravel 5 {{ $title }} construction wil be escaped.
If you don't want the data to be escaped, you may use the following syntax:
{!! $title !!}
Read more about Blade control structures: http://laravel.com/docs/5.0/templates#other-blade-control-structures
view
#if(Session::has('success'))
<div class="alert alert-success">
{!! Session::get('success')!!}
</div>
#endif
Controller
public function store(Request $request)
{
$this->validate($request, [
'product_code' => 'required|unique:products',
'product_name' => 'required',
'description' => 'required',
'price' => 'required',
'brand_id' => 'required',
'category_id' => 'required',
]);
$product = Product::create([
'product_code' => $request->input('product_code'),
'product_name' => $request->input('product_name'),
'description' => $request->input('description'),
'price' => $request->input('price'),
'brand_id' => $request->input('brand_id'),
'category_id' => $request->input('category_id'),
]);
return redirect()->route('products.index')->with('success',
"The product <strong>".$product->product_name."</strong> has successfully been created.");
}
I would rather not deal with decorators as my form design is not exactly straight forward, but i would like to keep the functionality of validating the forms.
So i have it set up where sub forms are working correctly, but when i try to style it manually in my viewscript i get the name without the parent. I've seen other posts that are similar, but i haven't found a solution.
Example:
This is in my view script
<?php echo $this->form->username->renderViewHelper();?>
I then get
<input type="text" value="" id="username" name="username">
When rendered. It should be
<input type="text" value="" id="form1-username" name="form1[username]">
How do i get that form1 portion?
Thanks!
Edit
Ok, so i found one way.
By using belongTo, it works:
$form1->addElements(array(
new Zend_Form_Element_Text('username', array(
'belongsTo' => 'form1',
'required' => true,
'label' => 'Username:',
'filters' => array('StringTrim', 'StringToLower'),
'validators' => array(
'Alnum',
array('Regex',
false,
array('/^[a-z][a-z0-9]{2,}$/'))
)
))
));
Is there a better way to do this or is this the only way?
Edit2
public function prepareSubForm($spec){
if (is_string($spec)) {
$subForm = $this->{$spec};
} elseif ($spec instanceof Zend_Form_SubForm) {
$subForm = $spec;
} else {
throw new Exception('Invalid argument passed to ' .
__FUNCTION__ . '()');
}
$this->setSubFormDecorators($subForm)
->addSubmitButton($subForm)
->addSubFormActions($subForm);
return $subForm;
}
public function setSubFormDecorators(Zend_Form_SubForm $subForm){
$subForm->setDecorators(array(
'FormElements', \\<--- I tried to change this to PrepareElements before.
array('HtmlTag', array('tag' => 'dl',
'class' => 'zend_form')),
'Form',
));
return $this;
}
I believe you can get your desired output just by using:
<?php echo $this->form->username; ?>
I get the expected output when calling this without renderViewHelper. This is also without any special code for decorators or preparing sub forms. All I had to do was add belongsTo to the form element.
UPDATED:
If you set this to be your default decorator, you can eliminate the dd/dt tags from rendering, instead it will use a div. Then you may be closer to getting the custom output you want. You can change the tag in HtmlTag from div to whatever tag you would like to wrap your elements in. This is what I use mostly:
array(
'ViewHelper',
'Errors',
array('Description', array('tag' => 'p', 'class' => 'description')),
array('HtmlTag', array('tag' => 'div', 'class' => 'form-div')),
array('Label', array('class' => 'form-label', 'requiredSuffix' => '*'))
);
This is the default for Zend Framework:
array(
'ViewHelper',
'Errors',
array('Description', array('tag' => 'p', 'class' => 'description')),
array('HtmlTag', array('tag' => 'dd', 'id' => array('callback' => $getId)))
array('Label', array('tag' => 'dt'))
);
Note that file, and submit/button elements use different decorators.
Also see this answer