Related
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'm trying to submit a form to a store function. The problem is, the foreignKey (degree_id) keeps being set to ?(null) even though I set it to the id in the route.
Form:
{!! Form::open(['url' => 'degrees/{{ $Degree->id }}', 'method' => 'POST']) !!}
<div class="form-group">
{{Form::label('title', 'Title')}}
<br>
{{Form::text('title', '', ['class' => 'form-control', 'placeholder' => 'Title'])}}
</div>
#error('title')
<small class="text-danger">{{ $message }}</small>
#enderror
Route:
Route::post('degrees/{degree}', 'ModuleController#store');
Store function:
public function store(Request $request)
{
$this->validate($request, [
'title' => 'required',
'desc' => 'required',
'long_desc' => 'required',
'hours' => 'required',
'credits'=> 'required',
]);
$module = new Module;
$module->title = $request->input('title');
$module->desc = $request->input('desc');
$module->long_desc = $request->input('long_desc');
$module->hours = $request->input('hours');
$module->credits = $request->input('credits');
$module->degree_id = $request->route('id');
$module->save();
return redirect('/home/modules');
}
I've since set the degree_id to nullable, so I can create my module and have it display alright, but for future functionality I'd like it to be set to the degree_id. Any idea whats not working here? I do have model-relationships set up, but I can't see how that'd impact it. Maybe something in the route itself?
You can't use Blade Mustache inside a php element, use this instead:
{!! Form::open(['url' => 'degrees/' . $Degree->id , 'method' => 'POST']) !!}
Your route parameter is not named id, you declared it in your route definition as the name degree ('degrees/{degree}').
$request->route('degree');
Change route from
Route::post('degrees/{degree}', 'ModuleController#store');
TO
Route::post('degrees/{id}', 'ModuleController#store');
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
PagesController.php
$id = $request->request->get('id');
$target = $request->request->get('target');
$EntityName = 'App\\Entity\\' . ucwords($slug);
$em = $this->getDoctrine()->getManager();
$cmf = $em->getMetadataFactory();
$classes = $cmf->getMetadataFor($EntityName);
if($request->request->get('target')){
$item = new $EntityName();
$item= $this->getDoctrine()->getRepository($EntityName)->find($id);
$formBuilder = $this->createFormBuilder($item);
foreach ($classes->fieldMappings as $fieldMapping) {
$formBuilder->add($fieldMapping['fieldName'], TextType::class, array('attr' => array('class' => 'form-control'), 'required' => true,));
}
$formBuilder->add('cancel', ButtonType::class, array('label' => 'Cancel','attr' => array('class' => 'cancel form-btn btn btn-default pull-right close_sidebar close_h')))
->add('save', SubmitType::class, array('label' => 'Save','attr' => array('id' => 'submit-my-beautiful-form','class' => 'form-btn btn btn-info pull-right','style' => 'margin-right:5px')));
$form = $formBuilder->getForm();
$form->handleRequest($request);
$response = new JsonResponse(
array(
'message' => 'Success',
'output' => $this->renderView('form.html.twig',
array(
'target' => $target,
'entity' => $item,
'form' => $form->createView(),
))), 200);
return $response;
} else {
$em = $this->getDoctrine()->getManager();
foreach ($classes->fieldMappings as $fieldMapping) {
$func = 'set'.$fieldMapping['fieldName'];
$args = $data['form['.$fieldMapping['fieldName'].']'];
$entity->$func($args);
}
$em->persist($entity);
$em->flush();
$response = new JsonResponse(array('id' => $data['form[id]']), 200);
return $response;
}
form.html.twig
<section class="content-header" style="margin-bottom:20px">
<h1 style="float:left;margin-bottom:30px">Create Entry </h1>
</section>
<section class="content" style="clear:left">
<div class="form-group">
{{ form_start(form) }}
{{ form_end(form) }}
</section>
My form is working well, when I fill it out and press the "Save" Button it is stored in the database.
When I leave all fields empty and press "Save" nothing is happening and I get a 500 Error
An exception occurred while executing 'INSERT INTO members (username,
password, email, is_active) VALUES (?, ?, ?, ?)' with params ["",
null, "", "1"]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column
'password' cannot be null
This is actually fine, because the fields are required, but the errors are not displayed in my form, even if I added "required" = "true".
So when using the form builder, you have access to:
$form->isSubmitted()
and
$form->isValid()
As a result you can do something like:
if ($form->isSubmitted() && $form->isValid()) {
// save to database
}
We want to check the form is submitted before we check it's valid as if the form isn't submitted, it's unnecessary to check it's valid as it will be false.
This is going to prevent your MySQL error because your form technically isn't valid and you're trying to flush invalid data. We obviously only want to save our data when it's valid.
Of course, if the form fails you can return the view and in the template, you have access to
{{ form_errors() }}
This will probably cover what you need but you could also pass to your template something like
'formHasErrors' => $form->isSubmitted() && !$form->isValid(),
and then in your template
{% if formHasErrors %}
Something else you may want to do, this allows you to have more control over your fields, is separate out the field out like below:
{{ form_start(form) }}
{{ form_label(form.name) }}
{{ form_errors(form.name, {'attr': {'class': 'form-input'}}) }}
{{ form_widget(form.name) }}
{{ form_end(form) }}
It is very important you catch errors and handle them correctly. Your implementation didn't verify the form is valid which is why you were getting 500 errors.
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