I need to use a productRepository method from within a custom twig extension. I can use standard methods like 'findOneBy' but if I define a custom method in productRepository (say returnVariants() ) then I get this error:
An exception has been thrown during the rendering of a template ("Undefined method 'returnVariants'. The method name must start with either findBy or findOneBy!") in SyliusWebBundle:Frontend/Homepage:main.html.twig at line 16.
The code of the custom twig extension:
namespace Sylius\Bundle\WebBundle\Twig;
use Symfony\Bridge\Doctrine\RegistryInterface;
class ProductExtension extends \Twig_Extension
{
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
public function getFunctions()
{
return array(
'product_func' => new \Twig_Function_Method($this, 'productFunc'),
);
}
public function productFunc($id)
{
/* This works */
$product = $this->doctrine->getRepository('SyliusCoreBundle:Product')
->findOneBy(array('id' => $id));
/* This doesn't */
$product = $this->doctrine->getRepository('SyliusCoreBundle:Product')->returnVariants();
return $product->getPrice();
}
Thank you very much for your help!
Make sure your entity is using the custom Repository
/**
* #ORM\Entity(repositoryClass="Sylius\...\ProductRepository")
**/
class Product { ... }
Also try clearing your cache
I would suggest not making a custom twig function.
Call this function in the controller and pass the results to twig
Related
As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.
In my application a company has their own subdomain. Im listening to kernel request event and setting the Company Filter(Doctrine Filter) parameter based on the company matching the subdomain.
public function setCompanyFilter($companyId)
{
/** #var EntityManager $entityManager */
$entityManager = $this->container->get('doctrine')->getManager();
$filters = $entityManager->getFilters();
$companyFilter = $filters->isEnabled('company_filter')
? $filters->getFilter('company_filter')
: $filters->enable('company_filter');
$companyFilter->setParameter('company', $companyId);
}
The issue im having is that on twig extensions(filter/functions) the parameter is not setted. If i set the value before execute a filter/function everything works as expected.
Is there any way to execute some code before every twig filter/function/tag? Like listening to an twig event? Or how can i solve this issue without calling the setCompanyFilter on every twig filter/function/tag.
Thanks
Why not set the custom value in the same event (i.e. kernel.request) that you are already listening to?
I assume you are using a custom twig extension. If not extend the filter/function you are already using and do the same:
<?php
// src/AppBundle/Twig/AppExtension.php
namespace AppBundle\Twig;
class AppExtension extends \Twig_Extension
{
private $customParameter;
public function getFilters()
{
return array(
new \Twig_SimpleFilter('price', array($this, 'priceFilter')),
);
}
public function priceFilter($number, $decimals = 0, $decPoint = '.', $thousandsSep = ',')
{
$price = number_format($number, $decimals, $decPoint, $thousandsSep);
$price = '$'.$price;
return $price;
}
public function getName()
{
return 'app_extension';
}
public function setCustomParameter($parameter)
{
$this->customParameter = $parameter;
}
}
Inject the twig extension into your current listener and then call the method setCustomParameter, inject your custom parameter for use later in the request lifecycle, and then just call the filter/function as your normally would in your existing twig template.
My project has a core bundle which holds the main templates,
and I have optional features implemented as bundles.
Different clients get copies of the project with different bundles(=features) activated.
One thing that the feature bundles need to be able to do is add things to the main templates (in the core bundle).
Obviously I don't want the templates in the core bundle to "know" about the optional bundles, so I want to have "hooks" in the templates that other bundles can pour content into.
I have one solution, which seems to work. I would appreciate feedback/suggestions.
I have a twig function which gets a "hook" name. I place that function at different locations in my template.
That function raises an event (through the event dispatcher) that collects html snippets from various event listeners (the feature bundles are responsible for those listeners), joins those snippets and returns them.
code:
twig function:
new \Twig_SimpleFunction('template_hook', array( $this, 'templateHookFunction' ), array('is_safe' => array('html'))),
public function templateHookFunction($hookName)
{
$hookData = $this->eventDispatcher->dispatch(
TemplateHookEvent::TEMPLATE_HOOK,
new TemplateHookEvent($hookName)
)->getData();
return $this->hookDataRenderer->render($hookData);
}
the event:
<?php
namespace CRM\TwigBundle\Event;
use CRM\TwigBundle\Hook\HookData;
use Symfony\Component\EventDispatcher\Event;
class TemplateHookEvent extends Event
{
const TEMPLATE_HOOK = 'crm_twig.template_hook';
private $hookName;
/** #var HookData $data */
private $data;
function __construct($hookName)
{
$this->hookName = $hookName;
$this->data = new HookData();
}
/**
* #return mixed
*/
public function getHookName()
{
return $this->hookName;
}
/**
* #return HookData
*/
public function getData()
{
return $this->data;
}
/**
* #param HookData $data
*/
public function setData(HookData $data)
{
$this->data = $data;
}
public function addData($label, $html, $overwrite = true)
{
if ($overwrite || !isset($this->data[$label])) {
$this->data[$label] = $html;
} else {
$this->data[$label] .= $html;
}
}
}
an example for a listener:
<?php
namespace CRM\ComputersBundle\EventListener;
use CRM\TwigBundle\Event\TemplateHookEvent;
class TemplateHookListener {
public function onTemplateHook(TemplateHookEvent $event)
{
$event->addData('computersBundle', '<li>CAKE</li>');
}
}
HookData is just a wrapper for an array (for now), each listener can push to it its own snippet.
and the HookDataRenderer just joins(implode) the array, and returns the result.
I don't see the point in your architecture. Why do you want such a hook system ? Why not using, for example, controller embedding in Twig as described here ?
Ok, I was trying to create twig extension with dependencies on other service (security.context) and got some troubles. So, here is my service declaration:
acme.twig.user_extension:
class: Acme\BaseBundle\Twig\UserExtension
arguments: ["#security.context"]
tags:
- { name: twig.extension }
and here's my class
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
protected $context;
public function __construct(SecurityContext $context){
$this->context = $context;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->context->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}
Finally, I have an error:
FatalErrorException: Error: Call to a member function getUser() on a non-object in \src\Acme\BaseBundle\Twig\UserExtension.php line 27
I guess that security.context service is not initialized yet, then i get an error.
Could anyone tell, please, is there are ways to load service manually, or any better solutions for an issue?
Thanks a lot.
I use Symfony 2.5.*
UPD:
I've also found this notice in symfony docs
Keep in mind that Twig Extensions are not lazily loaded. This means that there's a higher chance that you'll get a CircularReferenceException or a ScopeWideningInjectionException if any services (or your Twig Extension in this case) are dependent on the request service. For more information take a look at How to Work with Scopes.
Actually, I have no idea about how to do it correct..
You are calling $this->getAbcData() when constructing Twig_SimpleFilter. But you have to pass a callable as argument.
public function getFunctions() {
return array (
'getAbcData' => new \Twig_SimpleFunction( 'getAbcData', array( $this, 'getAbcData' ))
);
}
Leo is also right. You should check first if getToken() is returning an object before trying getToken()->getUser().
You can also pass the user to the function as a parameter in twig: {{ getAbcData(app.user) }}. This way the function is more generic and could be used for any user, not just the currently logged in one.
This should probably work. The error message means that getToken() is not an object so you have to test if getToken() is an object before testing if getUser() is also is an object.
public function getAbcData()
{
$token = $this->context->getToken();
if (!is_object($token) || !is_object($token->getUser())) {
return null;
}
return array(
'data_array' => $user->getData(),
);
}
You need to change your twig extension to have the container not the security context passed into the constructor.
Twig_Extensions are special in that the normal rule of don't pass in the container but instead pass in only what you need often doesn't apply as it causes problems due to scope issues.
So change your extension to be like this.
// acme/basebundle/twig/userextension.php
namespace Acme\BaseBundle\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\SecurityContext;
use Acme\UserBundle\Entity\User;
class UserExtension extends \Twig_Extension
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container){
$this->container = $container;
}
public function getFunctions()
{
return array(
'getAbcData' => new \Twig_SimpleFunction('getAbcData', $this->getAbcData()),
);
}
public function getAbcData()
{
if ( !is_object($user = $this->container->get('security.context')->getToken()->getUser()) || !$user instanceof User){ return null; }
return array(
'data_array' => $user->getData(),
);
}
public function getName()
{
return 'user_extension';
}
}
I want to create event listener that add some results of db query to all symfony actions
for example:
class BlogController extends Controller
{
/**
* #Route("/blog/")
* #Template()
*/
public function indexAction()
{
....
return array(
'entries' => $posts
);
}
}
This controller is passing entries variable to the view, I want to create listener that take the returned value of all actions and inject another index to the returned array to be (for example)
array(
'entries' => $posts,
'categories' => $categories
);
so I can call the $categories var from any where in my application views
I hope my question is clear to you guys. Thanks in advance.
You should consider creating a global variable or twig extension to make categories available in your templates, you can't do that by using events (since the template is parsed inside the controller, not before/after it)
This approach, although valid and commonly used in some frameworks, is not very common in Symfony as it suits more MVC than HMVC architecture.
I would suggest you a different one with the same result:
Instead of adding parameter to every controller return, render another controller which returns just a subview of what you're trying to show. Simple example:
// article/index.html.twig
<div class="category-bar">{{ render(controller('MyVendorMyBundle:CategoryController:bar')) }}</div>
<div class="article-list">
{% for article in articles %>
{# Print article here #}
{% endfor %}
</div>
// CategoryController
class CategoryController extends Controller
{
/**
* #Template
*/
public function barAction()
{
return ['categories' => $this->fetchCategoriesSomehow()];
}
}
So when you render your article list action, twig will fire a subrequest to render categories bar above it.
Furthermore, if you don't like making subrequests, nothing stops you from creating a twig extension service which would fetch categories and render template for you.
In most cases I would go with #Wouter J's suggestion and create a twig extension or a global variable.
However, what you want to do is actually possible (regardless if that's the right solution or not).
The #Template annotation has a vars attribute, which lets you to specify which atttributes from the request should be passed to the template:
/**
* #ParamConverter("post", class="SensioBlogBundle:Post")
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"post"})
*/
public function showAction()
{
}
Note, that request attributes can be set by you:
$request->attributes->set('categories', []);
So, you could implement a listener which would set the categories attribute on the request and than configure the vars on the #Template annotation:
/**
* #Template("SensioBlogBundle:Post:show.html.twig", vars={"categories"})
*/
public function showAction(Post $post)
{
}
Have a look at the TemplateListener from the SensioFrameworkExtraBundle for more insight. The listener defines template vars on kernel.controller and uses them to render the view on kernel.view.
You could avoid defining vars on the annotation if your listener was registered after the TemplateListener::onController(). It would have to add categories to the _template_vars request attribute.
Use Twig extension to create function that will return list of available categories
<?php
class CategoriesExtension extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getCategories', [$this, 'getCategoriesList'])
];
}
/**
* #return null|string
*/
public function getCategoriesList()
{
return CategoryQuery::create()->find();
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'list_categories';
}
}
You can pass parameter to function if You would like do some conditions on query.
The trick is to get the twig service in your listener and then use addGlobal to add your categories
namespace Cerad\Bundle\CoreBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyEventListener extends ContainerAware implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::CONTROLLER => array(
array('doCategories', -1100),
);
}
public function doCategories(FilterControllerEvent $eventx)
{
// Query your categories
$categories = array('cat1','cat2');
// Make them available to all twig templates
$twig = $this->container->get('twig');
$twig->addGlobal('categories',$categories);
}
# services.yml
cerad_core__my__event_listener:
class: '%cerad_core__my__event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }