I have a route with 2 parameters:
BBBundle_blog_show:
pattern: /{id}/{slug}
defaults: { _controller: BloggerBlogBundle:Blog:show }
requirements:
_method: GET
id: \d+
Both params are properties of an object blog.
I would like to set up a custom mapper (route generator), so that I can write this:
{{ path('BBBundle_blog_show', {'blog': blog}) }}
instead of this:
{{ path('BBBundle_blog_show', {'id':blog.id, 'slug':blog.slug) }}
This is what I came up with eventually:
I implemented by own generator base class that looks for 'object' parameter and tries to get required parameters from that object.
//src/Blogger/BlogBundle/Resources/config/services.yml
parameters:
router.options.generator_base_class: Blogger\BlogBundle\Routing\Generator\UrlGenerator
//src/Blogger/BlogBundle/Routing/Generator/UrlGenerator.php
namespace Blogger\BlogBundle\Routing\Generator;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
use Doctrine\Common\Util\Inflector;
/**
* UrlGenerator generates URL based on a set of routes.
*
* #api
*/
class UrlGenerator extends BaseUrlGenerator
{
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute)
{
if (isset($parameters['object']) && is_object($parameters['object'])) {
$object = $parameters['object'];
$parameters = array_replace($this->context->getParameters(), $parameters);
$tparams = array_replace($defaults, $parameters);
$requiredParameters = array_diff_key(array_flip($variables), $tparams);
$parameters = $this->getParametersFromObject(array_flip($requiredParameters), $object);
}
return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $absolute);
}
protected function getParametersFromObject($keys, $object)
{
$parameters = array();
foreach ($keys as $key) {
$method = 'get' . Inflector::classify($key);
if (method_exists($object, $method)) {
$parameters[$key] = $object->$method();
}
}
return $parameters;
}
}
Now I can write: {{ path('BBBundle_blog_show', {'object': blog}) }} and it will get required parameters (id, slug) from object.
A while ago, I decided I was annoyed by being unable to pass objects as route parameters. I had to concern myself with knowledge of routes and the exact parameter values within templates and other things generating those routes.
I've build this bundle for symfony, which allows you to use and extend this ability (Symfony 2.7 and higher). Please take a look: https://github.com/iltar/http-bundle. It's also available on Packagist as iltar/http-bundle.
The best thing about this bundle is that you don't need to use another router object or generator. It's just turning on the bundle, adjusting the config to your needs if the defaults don't work out for your preferences and you're good to go. The readme should explain everything you need to know but here's a snippet:
Old style:
/**
* #Route("/profile/{user}/", name="app.view-profile")
*/
public function viewProfileAction(AppUser $user);
// php
$router->generate('app.view-profile', ['user' => $user->getId()]);
// twig
{{ path('app.view-profile', { 'user': user.id }) }}
{{ path('app.view-profile', { 'user': user.getid }) }}
{{ path('app.view-profile', { 'user': user.getId() }) }}
{{ path('app.view-profile', { 'user': user[id] }) }}
New style:
// php
$router->generate('app.view-profile', ['user' => $user]);
// twig
{{ path('app.view-profile', { 'user' : user }) }}
Related
I have an Entity named Page that can be a callToAction (boolean) and I would like to display the Page Entity with callToAction == false on one subMenu and the Page Entity with callToAction == true on another subMenu. I have a CRUD for the Page Entity. So the Dashboard would be something like that:
MenuItem::subMenu('Page', 'far fa-file-alt')->setSubItems([
MenuItem::linkToCrud('Page', 'fa fa-alt', Page::class),
MenuItem::linkToCrud('Call To Action', 'fa fa-file-alt', Page::class),
])
But I don't know where to put the dql to display the entities I want (callToAction true or false) and I don't even know if it's possible, but I know it was with Easy Admin 2, that's why I wonder.
I also would like that on the NEW Action, when you're on the Page with callToAction == true, when you create the new Entity Page from here, that the callToAction is set to true immediatly and the User doesn't even see the field. Still don't know if it's possible.
Thanks :)
EDIT: So i've found that I can use createIndexQueryBuilder() to display on the index exactly the entities, and it works well but I don't know how to call two different createIndexQueryBuilder depending of the subMenu we display. I tried doing a custom action and using createQueryBuilder but I don't have the params searchDto, etc:
public function configureActions(Actions $actions): Actions
{
$indexIsCallToAction = Action::new('indexIsCallToAction', 'Index Call To Action', 'fa fa-send')
->linkToCrudAction('indexIsCallToAction');
$actions->add(CRUD::PAGE_INDEX, $indexIsCallToAction);
return $actions;
//return parent::configureActions($actions); // TODO: Change the autogenerated stub
}
public function indexIsCallToAction(AdminContext $context,SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields, FilterCollection $filters){
$response = $this->get(EntityRepository::class)->createQueryBuilder($searchDto, $entityDto, $fields, $filters);
return $response;
}
So it doesn't work.
As a dashboard controller is an ordinary controller you can do something like this:
public function __construct(PageRepository $pageRepo)
{
$this->pageRepo = $pageRepo;
}
public function configureMenuItems(): iterable
{
$submenuItems = [];
if (null !== $pages = $this->pageRepo->findBy(["callToAction" => true ])) {
foreach ($pages as $page) {
$submenuItems[] = MenuItem::linkToCrud('Call To Action', 'fa fa-file-alt', Page::class);
}
}
yield MenuItem::subMenu('Page Submenu with callToAction', 'far fa-file-alt')->setSubItems($submenuItems);
$submenuItems = [];
if (null !== $pages = $this->pageRepo->findBy(["callToAction" => false ])) {
foreach ($pages as $page) {
$submenuItems[] = MenuItem::linkToCrud('Page', 'fa fa-alt', Page::class);
}
}
yield MenuItem::subMenu('Other Page Submenu', 'far fa-file-alt')->setSubItems($submenuItems);
}
In my entity I defined a field color with a callback. The colors can only be selected in the COLORS list (const in this class)
/**
* #ORM\Entity(repositoryClass="App\Repository\EventTagRepository")
*/
class EventTag
{
const COLORS = [
"primary"=>"primary",
"secondary"=>"secondary",
"success"=> "success",
"danger"=>"danger",
"warning"=>"warning",
"info"=>"info",
"light"=>"light",
"dark"=>"dark"
];
/**
* #ORM\Column(type="string", length=255)
* #Assert\Choice(callback="getColors")
*/
private $color;
public function getColors()
{
return $this::COLORS;
}
When I'm creating the form in easy-admin, I'd like to access this callback in the choice type options to prevent the user to choose a wrong color.
EventTag:
class: App\Entity\EventTag
list:
actions: ['-delete']
form:
fields:
- { type: 'group', label: 'Content', icon: 'pencil-alt', columns: 8 }
- 'name'
- { property: 'color', type: 'choice', type_options: { expanded: false, multiple: false, choices: 'colors'} }
Unfortunately in the type_options I didn't find a way to access the entity properties, instead of searching for getColors(), IsColors(), hasColors() methods, it only reads the string.
Is it possible to do it another way ?
The callback refers to an entity const:
you can use
#Assert\Choice(choices=EventTag::COLORS)
in the PHP entity and
choices: App\Entity\EventTag::COLORS
in the YAML config
the callback refers to a more specific value:
you need to manually extend the AdminController
public function createCategoryEntityFormBuilder($entity, $view)
{
$formBuilder = parent::createEntityFormBuilder($entity, $view);
$field = $formBuilder->get('type');
$options = $field->getOptions();
$attr = $field->getAttributes();
$options['choices'] = $formBuilder->getData()->getTypeLabels();
$formBuilder->add($field->getName(), ChoiceType::class, $options);
$formBuilder->get($field->getName())
->setAttribute('easyadmin_form_tab', $attr['easyadmin_form_tab'])
->setAttribute('easyadmin_form_group', $attr['easyadmin_form_group']);
return $formBuilder;
}
As $formBuilder->add erases attributes we need to set them again manually. Probably it can be skipped if you are not using Groups/Tabs, otherwise it will throw Exception saying that field was already rendered.
I'm currently discovering Symfony, and I have some trouble calling a function from one of my controllers.
> class CsvController extends Controller {
public function generateCsvAction(){
$conn=$this->get('database_connection')
$results=$conn->query("
SELECT *
FROM external_attribute;
")
$response=new StreamedResponse();
$response->setCallback(function() use($results)){
$handle=fopen("/home/basile/Documents/backend/src/CampaignBundle/Controller/test.csv","w+");
fputcsv($handle,array('test1
test2,
test3,
test4,
test5,
test6,
test7,
test8')
),';');
}
fclose($handle);
}
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition','attachment; filename="test.csv"');
return $response;
}
I already set everything in my routing.yml :
export_csv:
defaults: { _controller: CampaignBundle:Controller:CsvController.php }
and now I want to call it from a button in a file named "index.html.twig" . I know we can render from the controller some variables and array, but here i directly want to call a function
If you have any kind of idea, it would be very welcomed !
To directly call a controller from your template :
{{ render(controller(
'NameOfYourBundle:NameOfYourClass:NameOfYourFunction'
)) }}
If controller needs parameters (eg) :
{{ render(controller(
'NameOfYourBundle:NameOfYourClass:NameOfYourFunction', { 'id': 3 }
)) }}
you can execute the function directly from your view:
{% render "YourBundle:Csv:generateCsv" with { 'url': 'export_csv' } %}
By JS and AJAX :
$("#button").on('click', function() {
$.ajax({
url: {{render(controller("YourBundle:Csv:generateCsv")) }},
success: function(result){
...
}});
});
public indexFunction(){
$var = 'Apple';
//set $var global
}
So that $var can be accessed in base template.
if you want it as static value just put it in the config file:
# app/config/config.yml
twig:
globals:
var: 'Apple'
if you want it as dynamic variable you can give the twig the service id:
twig:
globals:
# the value is the service's id
var: '#AppBundle\Service\yourData'
You have to pass the variable to the template:
// AppBundle/Controller/DefaultController.php
public indexFunction()
{
$var = 'Apple';
return $this->render('index.html.twig', array(
'var' => $var,
));
}
If you really want to have a global variable available to all of your templates you should set it in your twig config:
# app/config/config.yml
twig:
globals:
var: 'value'
after that you can write out the value of the variable in the twig template with {{ var }}
EDIT:
Or finally example using session - saving the variable into the session
// AppBundle/Controller/DefaultController.php
public indexFunction()
{
$var = 'Apple';
$session = $this->get('session');
$session->set('var', $var);
...
}
after that you can retrieve it in the twig template like this
{# /app/Resources/views/base.html.twig #}
{{ app.session.get('var') }}
Dynamic:
In the bundle path (Example: src/AppBundle) add a file called "YourNameTwigExtension.php". This file will be a class (YourNameTwigExtension) that extends "\Twig_Extension" class and implements the interface "\Twig_Extension_GlobalsInterface". Inside the "YourNameTwigExtension" class implement the getGlobals() method. Example:
//....
class YourNameTwigExtension extends \Twig_Extension implements \Twig_Extension_GlobalsInterface {
//...
public function getGlobals() {
$data = array();
$data['user'] = $this->session->get('user');
$data['menu'] = $this->session->get('menu');
$data['actions'] = $this->session->get('actions');
$data['view'] = $this->session->get('view');
return $data;
}
}
In TWIG you will use: {{ user }}, {{ menu }}, {{ actions }}, etc...
Warning: Add in app/config/services.yml:
twig.extension.yourname_twig_extension:
class: YourBundle\YourNameTwigExtension
tags:
- { name: twig.extension }
Static:
If you need to use the static variables, in app/config/parameters.yml:
twig:
globals:
portal_name: 'Portal'
portal_img_logo: logo.png
portal_favicon: favicon.ico
logowidth: 350px
Symfony Doc: How to Inject Variables into all Templates (i.e. global Variables)
Sorry my English.. Good Work!
I'm using jeditable plugin for JavaScript and I want to implement it in my Symfony2 project. I want to edit a name with the plugin and that name to be edited in the database, too, not the change to be gone when I refresh the page, but in my case, it's gone. :(
I'm almost sure that the controller shouldn't be in that way and the problem is from it, but how exactly to write it? Here it is:
public function editCategoryAction(Request $request, $id)
{
$category = $this->repository->find($id);
$form = $this->createForm(new CategoryType(), $category);
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
$this->em->persist($category);
$this->em->flush();
return $this->redirect($this->generateUrl('categories'));
}
}
return $this->render(
'AcmeBudgetTrackerBundle:Categories:categories.html.twig', array(
'form' => $form->createView()));
}
This is my template:
<a href="{{ path('edit_category', { 'id': cat.id}) }}">
<strong class="edit">
{{ cat.name }}
</strong>
</a>
<script>
var token = "{{form._token.vars.value}}";
var path = "{{ path('edit_category', { 'id': cat.id}) }}";
</script>
And this is in the .js file:
(function(){
$('.edit').editable(function (value, settings) {
var data = {};
data[this.id] = value;
data["_token"] = token;
console.log(path);
console.log(data);
$.post(path, data);
return(value);
}, {
indicator:'Saving...'
});
}) ();
The output in the console looks fine:
/BudgetTracker/web/app_dev.php/edit_category/52
Object {: "Edu", _token: "9d29860b59ccafbc265ea12346c91fa7e378cc97"}
but the problem is that nothing is posted to the database and when I hit refresh the change I made is gone.
Can you please help me to solve this? Thanks in advance! :)
I think you don't need to use the form component here, you only want to handle a string. So I'll explain a way to do.
JavaScript:
$('.edit').editable(path);
Controller:
public function editCategoryAction(Category $category)
{
//check if exists/valid
//get the text sent from jeditable
$name = $this->request->get('value');
$category->setName($name);
$em = $this->getDoctrine()->getManager();
$em->persist($category);
$em->flush();
//return the name value to jeditable so it can display it
return new Response($name);
}
Twig:
<strong class="edit">{{ cat.name }}</strong>
<script>
var path = "{{ path('edit_category', { 'id': cat.id}) }}";
</script>
jEditable sends edited text named as 'value', so you can get it in the controller. In your controller, you implicitly use paramconverter to get the category from the id in the URL. And that should be OK now.
Note that you can use FORJsRoutingBundle, if you want to avoid mixing twig with javascript in order to access path.