Parse Smarty template without triggering calls to user plugins - symfony

I'm writing a (Symfony2) SmartyBundle extension to support Assetic.
To support the stylesheets feature I've registered a block plugin called stylesheets:
{stylesheets
assets='#SmartyBundle/Resources/public/css/*'
debug=true}
{$asset_url}
{/stylesheets}
This plugin gets called properly and everything work as expected when the Symfony/assetic cache is created.
The problem arises When the Symfony cache is empty and Assetic loads every template file resource and asks the template engine to retrieve a PHP array with the tokens found in the stylesheets tag. The class called to retrieve the array is SmartyFormulaLoader.
<?php
class SmartyFormulaLoader implements \Assetic\Factory\Loader\FormulaLoaderInterface
{
public function load(ResourceInterface $resource)
{
// raw template content
$content = $resource->getContent();
// a FileLoaderImportCircularReferenceException is throw here
$smartyParsed = $this->smarty->fetch('string: '.$content);
// build an array with tokens extracted from the block function
$formulae = $this->extractStylesheetsTokens($smartyParsed);
return $formulae;
}
When $smarty->fetch() is called in the load() method an exception is thrown: Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException: Circular reference detected in "." ("." > ".").
This is caused by the Smarty template being parsed/compiled and the stylesheets plugin being called again.
So I'm asking if Smarty provides a template parser that extracts the block function tokens (without calling the stylesheets plugin) so I can feed Assetic. Or any other solution that I may be missing to solve this.
Thanks.

After a little chat with Smarty dev #rodneyrehm we came to the conclusion that:
Should parse raw template source instead of compiling the template and parsing it
Should write my own parser (instead of using some Smarty internal helper)
So, for this particular case we came up with this implementation:
<?php
class SmartyFormulaLoader
{
public function load(ResourceInterface $resource)
{
// template source
$templateSource = $resource->getContent();
// ask Smarty which delimiters to use
$ldelim = $smarty->left_delimiter;
$rdelim = $smarty->right_delimiter;
$_ldelim = preg_quote($ldelim);
$_rdelim = preg_quote($rdelim);
// template block tags to look for
$tags = implode('|', array('stylesheets', 'javascripts', 'image));
// match every assetic tag
if (preg_match_all('#'.$_ldelim.'(?<type>'.$tags.').*?'.$_rdelim.'#s', $templateSource, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
// extract block attributes
if (preg_match_all('#(?<key>[a-zA-Z0-9_]+)\s*=\s*(["\']?)(?<value>[^\2]*?)\2(\s|'.$_rdelim.')#s', $match[0], $_matches, PREG_SET_ORDER)) {
$t = array(
'type' => $match['type'],
'attributes' => array(),
);
foreach ($_matches as $_match) {
if (empty($_match[2])) {
// make eval a little bit safer
preg_match('#[^\w|^\.]#', $_match['value'], $evalMatches);
$_match['value'] = ($evalMatches) ? null : eval(sprintf('return %s;', $_match['value']));
}
$t['attributes'][$_match['key']] = $_match['value'];
}
// call some Assetic methods with the extracted attributes
$formulae += $this->buildFormula($match['type'], $t['attributes']);
}
}
}
return $formulae;
}
}
Full implementation is available here: https://github.com/noiselabs/SmartyBundle/blob/master/Assetic/SmartyFormulaLoader.php

Related

Symfony 2 - Twig templates - Entity function results

I am looking for best solution how to send value returned by one of entity function's in symfony2 to twig template.
The problem is connecting with getting file url for file uploaded according to "How to Handle File Uploads with Doctrine" manual (http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html). I was following the last example ("Using the id as the Filename").
In controller I am getting one of documents entity.
$document = $this->getDoctrine()->getRepository('AppBundle:Documents')->find($id);
and I provide entity details to twig template:
return $this->render('AppBundle:Documents:details.html.twig', array('document' => $document));
However in the template I need to get link to the file which is generated by getAbsolutePath() function.
public function getAbsolutePath()
{
return null === $this->link
? null
: $this->getUploadRootDir().'/'.$this->id.'.'.$this->link;
}
I may use in controller the following code:
return $this->render('AppBundle:Documents:details.html.twig', array('document' => $document, 'link' => $document->getAbsolutePath()));
but this solution does not seems to tidy for me, as I am already sending $document to twig. What would be your practical solution?
Its simple. In a Twig template you can simply do:
{{ document.getAbsolutePath() }}

How to use custom Twig filters in Twig template rendered from string?

I want to render Twig template from string (in fact from database but it doesn't matters) and method showed in this question works fine. But I can't use my custom filters, defined in custom Twig extension, which works in templates rendered in standard way (from files).
What should I do to allow string-templates use custom filters?
I'm getting template from string in this way:
$env = new \Twig_Environment(new \Twig_Loader_String());
echo $env->render(
"Hello {{ name }}",
array("name" => "World")
);
I've found in \Twig_Environment class method addFilter but my filters are defined in Twig extension so I don't know if it's proper method to use.
Solution:
Thanks to Luceos comments I've solved my problem just by using existing Twig engine instead of creating new one. When I set Twig Environment this way:
$twig = clone $this->get('twig');
$twig->setLoader(new \Twig_Loader_String());
then everything works as expected. (Solution found in http://www.techpunch.co.uk/development/render-string-twig-template-symfony2)
Use Symfony's Twig instantiated object and do not set up your own. Setting up your own environment leaves out all added functionality by Symfony.
For instance use the render function in controllers: http://symfony.com/doc/current/book/templating.html#index-8
Or call on the template service directly:
http://symfony.com/doc/current/book/templating.html#index-12
use Symfony\Component\HttpFoundation\Response;
$engine = $this->container->get('templating');
$content = $engine->render('AcmeArticleBundle:Article:index.html.twig');
return $response = new Response($content);

Symfony Multiple application interaction

In symfony 1.4, how to call an action of another application from the current action?
There's a blog post about that here:
http://symfony.com/blog/cross-application-links
There's a plugin for it:
https://github.com/rande/swCrossLinkApplicationPlugin
And there are some blogposts explaining it:
http://rabaix.net/en/articles/2009/05/30/cross-link-application-with-symfony
http://rabaix.net/en/articles/2009/07/13/cross-link-application-with-symfony-part-2
(Note: both are for symfony 1.2, but they should also work in 1.4)
To route to your frontend to your backend, there are three easy steps:
1. Add to your backend configuration the following two methods
These methods read the backend routing, and use it to generate routes. You'll need to supply the link to it, since php does not know how you configured your webserver for the other application.
.
// apps/backend/config/backendConfiguration.class.php
class backendConfiguration extends sfApplicationConfiguration
{
protected $frontendRouting = null;
public function generateFrontendUrl($name, $parameters = array())
{
return 'http://frontend.example.com'.$this->getFrontendRouting()->generate($name, $parameters);
}
public function getFrontendRouting()
{
if (!$this->frontendRouting)
{
$this->frontendRouting = new sfPatternRouting(new sfEventDispatcher());
$config = new sfRoutingConfigHandler();
$routes = $config->evaluate(array(sfConfig::get('sf_apps_dir').'/frontend/config/routing.yml'));
$this->frontendRouting->setRoutes($routes);
}
return $this->frontendRouting;
}
// ...
}
2. You can link to your application in such a fashion now:
$this->redirect($this->getContext()->getConfiguration()->generateFrontendUrl('hello', array('name' => 'Bar')));
3. Since it's a bit tedious to write, you can create a helper
function link_to_frontend($name, $parameters)
{
return sfProjectConfiguration::getActive()->generateFrontendUrl($name, $parameters);
}
The sfCrossLinkApplicationPlugin does this , this, but in a bit simpler fashion, you would be able to use a syntax similar to this:
<?php if($sf_user->isSuperAdmin()):?>
<?php link_to('Edit Blog Post', '#backend.edit_post?id='.$blog->getId()) ?>
<?php endif ?>
It would be something like this:
public function executeActionA(sfWebRequest $request)
{
$this->redirect("http:://host/app/url_to_action");
}
In Symfony each application is independent from the others, so if you need to call an action of another app, you need to request it directly.
Each app is represented by one main controller (frontend, backend, webapp), this controller takes care of the delivery of each request to the corresponding action (and lots of other things like filters, etc.).
I really recommend you to read this, it would be quite more explanatory: Symfony - Inside the Controller Layer

Drupal: cannot unset js file and use new js in theme directory

I want to copy a js file in my theme folder instead of hacking the module. This is my code:
/*update js files */
$scripts = drupal_add_js();
unset($scripts['module']['sites/all/modules/imagefield_crop/imagefield_crop.js']);
$scripts['module']['sites/all/themes/zen/zen/js/imagefield_crop.js'] = array('preprocess' => 1, 'cache' => 1);
$vars['scripts'] = drupal_get_js('header', $scripts);
IT works for lightbox2 but it doesn't work for imagefield_crop.js
I've cleaned all Drupal caches and browser cache but my browser continues to load the original js in the module directory.
thanks
Update: This is the array $scripts
['module']
...
[sites/all/modules/imagefield_crop/Jcrop/js/jquery.Jcrop.js] => Array
(
[cache] => 1
[defer] =>
[preprocess] => 1
)
Given the updated question after the discussion in the comments, it seems like you are mixing up the involved js files. Imagefield_crop adds two different ones:
jquery.Jcrop.js, which is an imported library file providing the crop functionality in general (in context of jquery) - normally, you should not have a reason to replace this.
'imagefield_crop.js', which is the one providing the 'bridging' to allow the above library to work properly in the Drupal context - my understanding was that you wanted to replace this one.
Both are needed for the functionality to work. Your posted code would only replace the second one, and unless you accidentally posted the wrong code snippet in your question update, it seems to work.
If you wanted to replace both (or only the first one), you'd need to extend/adjust your unsetting logic to do so.
Hello here is the possible solutions it might help though I've never done this before
/**
* Implementation of hook_theme_registry_alter().
* Based on the jquery_update module.
*
* Make this page preprocess function runs *last*,
* so that a theme can't call drupal_get_js().
*/
function MYMODULE_theme_registry_alter(&$theme_registry) {
if (isset($theme_registry['page'])) {
// See if our preprocess function is loaded, if so remove it.
if ($key = array_search('MYMODULE_preprocess_page',
$theme_registry['page']['preprocess functions'])) {
unset($theme_registry['page']['preprocess functions'][$key]);
}
// Now add it on at the end of the array so that it runs last.
$theme_registry['page']['preprocess functions'][] = 'MYMODULE_preprocess_page';
}
}
/**
* Implementation of moduleName_preprocess_hook().
* Based on the jquery_update module functions. *
* Strips out JS and CSS for a path.
*/
function MYMODULE_preprocess_page(&$variables, $arg = 'my_page', $delta=0) {
// I needed a one hit wonder. Can be altered to use function arguments
// to increase it's flexibility.
if(arg($delta) == $arg) {
$scripts = drupal_add_js();
$css = drupal_add_css();
// Only do this for pages that have JavaScript on them.
if (!empty($variables['scripts'])) {
$path = drupal_get_path('module', 'admin_menu');
unset($scripts['module'][$path . '/admin_menu.js']);
$variables['scripts'] = drupal_get_js('header', $scripts);
}
// Similar process for CSS but there are 2 Css realted variables.
// $variables['css'] and $variables['styles'] are both used.
if (!empty($variables['css'])) {
$path = drupal_get_path('module', 'admin_menu');
unset($css['all']['module'][$path . '/admin_menu.css']);
unset($css['all']['module'][$path . '/admin_menu.color.css']);
$variables['styles'] = drupal_get_css($css);
}
}
}
http://www.mediacurrent.com/blogs/remove-or-replace-jscss-page

Can Drupal change the template used based on a file extension in the url?

I am using Drupal 6.16 with a number of modules installed. I was trying to find out if there is a way to change the output of a node when a different file extension is added to the url. For example:
http://example.com/drupal?q=foo/bar - returns a normal drupal node
http://example.com/drupal?q=foo/bar.xml - returns xml output of the node
Is this even possible with Drupal? Do I have to hack the core code to get this working?
You should not need to hack the core code. There are probably several contributed modules that can do this for you.
To output an XML version of a node, check out the Views Bonus Pack module, which extends the Views module. It has basic export capabilities, including CSV, TXT, DOC, and XML. The documentation is brief, but there is a README.txt file in the views_bonus/export/ directory that gives the basic steps for creating a feed in a view that will output XML.
You can set the path for the feed, so while I don't believe the .xml extension will work, you could set up a path with an additional component like this:
http://example.com/drupal?q=foo/bar <-- normal output
http://example.com/drupal?q=foo/bar/xml <-- XML output
To change the template file that is used for a node based on the path, you can use a preprocess function in your template.php file to add a template suggestion based on the path. This takes a bit more understanding of how the template files work, but ultimately you'll have more control of the output than you will with a view.
Here is how I fixed this.
Add the custom_url_rewrite_inbound function to check for incoming request ending with .xml. If it finds a request ending with .xml it strips that off, so that the correct data can be located by the rest of the drupal machinery. It also sets 'subsite_xml_request' to true so that the appropriate theme template can be used later.
function custom_url_rewrite_inbound (&$result, $path, $path_language) {
if(preg_match('/\.xml$/', $path)) {
$search = preg_replace('/^(.*)\.xml$/', "$1", $path);
if ($src = drupal_lookup_path('source', $search, $path_language)) {
$_REQUEST['xml_request'] = true;
$result = $src;
}
}
Modify the phptemplate_preprocess_page function in your template.php to add additional '-xml' templates.
function phptemplate_preprocess_page(&$vars) {
if ($_REQUEST['xml_request']) {
if (module_exists('path')) {
$path = str_replace('/edit','',$_GET['q']);
$alias = drupal_get_path_alias($path);
if ($alias != $_GET['q']) {
$template_filename = 'page';
foreach (explode('/', $alias) as $path_part) {
$template_filename = $template_filename . '-' . $path_part;
$vars['template_files'][] = $template_filename . '-xml';
}
$vars['template_files'][] = 'page-xml';
}
}
}
}
Create the required page-xml.tpl.php

Resources