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 ?
Related
I have an issue where I need to instantiate a class in wordpress so that in the constructor I can use the function get_post_types and have that hook happen before the publish_post hook (which I assume is around the publish_CPT hooks).
Here is the code I have so far
class Transient_Delete {
/**
* #var array of all the different post types on the site
*/
private $postTypes;
/**
* #var array of wordpress hooks we will have to assemble to delete all possible transients
*/
private $wpHooks;
public static function init() {
$class = __CLASS__;
new $class;
}
public function __construct()
{
$this->postTypes = array_values( get_post_types(array(), 'names', 'and') );
$this->wpHooks = $this->setWpHooks($this->postTypes);
add_action('publish_alert', array($this, 'deleteAlertTest'));
}
private function setWpHooks($postTypes)
{
$hooks = array_map(function($postType) {
return 'publish_' . $postType;
}, $postTypes);
return $hooks;
}
private function deleteAlertTest($post)
{
$postId = $post->ID;
echo 'test';
}
}
add_action( 'wp_loaded', array( 'Transient_Delete', 'init' ));
Another note here is that this is in the mu-plugins directory.
note: "alert" of publish_alert is a custom post type.
Ok this was my fault, it looks like the hook publish_alert works fine if I change the deleteAlertTest function to public. Any idea on why having it be a private function has that effect? Its within the same class.
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.
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 }
I have been using Silex for my latest project and I was trying to follow along with the "How to Dynamically Modify Forms Using Form Events" in the Symfony cookbook. I got to the part that uses the entity field type and realized it is not available in Silex.
It looks like the symfony/doctrine-bridge can be added to my composer.json which contains the "EntityType". Has anyone successfully got entity type to work in Silex or run into this issue and found a workaround?
I was thinking something like this might work:
$builder
->add('myentity', new EntityType($objectManager, $queryBuilder, 'Path\To\Entity'), array(
))
;
I also found this answer which looks like it might do the trick by extending the form.factory but haven't attempted yet.
I use this Gist to add EntityType field in Silex.
But the trick is register the DoctrineOrmExtension form extension by extending form.extensions like FormServiceProvider doc says.
DoctrineOrmExtension expects an ManagerRegistry interface in its constructor, that can be implemented extending Doctrine\Common\Persistence\AbstractManagerRegistry as the follow:
<?php
namespace MyNamespace\Form\Extensions\Doctrine\Bridge;
use Doctrine\Common\Persistence\AbstractManagerRegistry;
use Silex\Application;
/**
* References Doctrine connections and entity/document managers.
*
* #author Саша Стаменковић <umpirsky#gmail.com>
*/
class ManagerRegistry extends AbstractManagerRegistry
{
/**
* #var Application
*/
protected $container;
protected function getService($name)
{
return $this->container[$name];
}
protected function resetService($name)
{
unset($this->container[$name]);
}
public function getAliasNamespace($alias)
{
throw new \BadMethodCallException('Namespace aliases not supported.');
}
public function setContainer(Application $container)
{
$this->container = $container['orm.ems'];
}
}
So, to register the form extension i use:
// Doctrine Brigde for form extension
$app['form.extensions'] = $app->share($app->extend('form.extensions', function ($extensions) use ($app) {
$manager = new MyNamespace\Form\Extensions\Doctrine\Bridge\ManagerRegistry(
null, array(), array('default'), null, null, '\Doctrine\ORM\Proxy\Proxy'
);
$manager->setContainer($app);
$extensions[] = new Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension($manager);
return $extensions;
}));
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