Is it possible to make global macros in Twig 3? - php

I want to upgrade my Twig from very old version (2.x or even 1.x) to 3.3. In old version macros imported in top level template are available in all extened and included templates. And I have a bunch of templates where I need my macros (100+ and many embed blocks). I don't want to import macros manually in evety template.
So, according to similar question Twig auto import macros by environment I tried to implement suggested solution, but it doesn't work.
Actually I tried this:
$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl);
I also tried this:
$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl->unwrap());
but I have same result. In templates fnc is defined but it's a Template object and I can not access macros. I get a fatal error when I try to do it:
Fatal error: Uncaught Twig\Error\RuntimeError: Accessing \Twig\Template attributes is forbidden
As I understand, in Twig 3 you can not just include macros using addGlobal.
Old Twig was added to our repository (was not ignored) and we probably will add to repository new Twig too, so it's possible to modify Twig's source code.
UPD:
When I try just to addGlobal my template with macros I get
Fatal error: Uncaught LogicException: Unable to add global "fnc" as the runtime or the extensions have already been initialized.
I've solved this problem using this solution (I've extended Environment class).

During some testing I found out you can still call the "functions" defined inside a macro with pure PHP
<?php
$wrapper = $twig->load('macro.html');
$template = $wrapper->unwrap();
echo $template->macro_Foo().''; //return a \Twig\Markup
With this in place you could write a wrapper around the macro and try to auto load them in a container.
First off we need an extension to enable and access the container
<?php
class MacroWrapperExtension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('macro', [$this, 'macro'], ['needs_environment' => true,]),
];
}
protected $container = null;
public function macro(\Twig\Environment $twig, $template) {
return $this->getContainer($twig)->get($template);
}
private function getContainer(\Twig\Environment $twig) {
if ($this->container === null) $this->container = new MacroWrapperContainer($twig);
return $this->container;
}
}
Next the container itself. The container is responsible to load and store/save the (auto loaded) macros in the memory. The code will try to locate and load any file in the map macros in your view folder.
template
|--- index.html
|--- macros
|------- test.html
|
<?php
class MacroWrapperContainer {
const FOLDER = 'macros';
protected $twig = null;
protected $macros = [];
public function __construct(\Twig\Environment $twig) {
$this->setTwig($twig)
->load();
}
public function get($macro) {
return $this->macros[$macro] ?? null;
}
protected function load() {
foreach($this->getTwig()->getLoader()->getPaths() as $path) {
if (!is_dir($path.'/'.self::FOLDER)) continue;
$this->loadMacros($path.'/'.self::FOLDER);
}
}
protected function loadMacros($path) {
$files = scandir($path);
foreach($files as $file) if ($this->isTemplate($file)) $this->loadMacro($file);
}
protected function loadMacro($file) {
$name = pathinfo($file, PATHINFO_FILENAME);
if (!isset($this->macros[$name])) $this->macros[$name] = new MacroWrapper($this->getTwig()->load(self::FOLDER.'/'.$file));
}
protected function isTemplate($file) {
return in_array(pathinfo($file, PATHINFO_EXTENSION), [ 'html', 'twig', ]);
}
protected function setTwig(\Twig\Environment $twig) {
$this->twig = $twig;
return $this;
}
protected function getTwig() {
return $this->twig;
}
public function __call($method_name, $args) {
return $this->get($method_name);
}
}
Last off we need to mimic the behavior I've posted in the beginning of the question. So lets create a wrapper around the macro template which will be responsible to call the actual functions inside the macro.
As seen the functions inside a macro get prefixed with macro_, so just let auto-prefix every call made to the macro wrapper with macro_
<?php
class MacroWrapper {
protected $template = null;
public function __construct(\Twig\TemplateWrapper $template_wrapper) {
$this->template = $template_wrapper->unwrap();
}
public function __call($method_name, $args){
return $this->template->{'macro_'.$method_name}(...$args);
}
}
Now inject the extension into twig
$twig->addExtension(new MacroWrapperExtension());
This will enable the function macro inside every template, which lets us access any macro file inside the macros folder
{{ macro('test').hello('foo') }}
{{ macro('test').bar('foo', 'bar', 'foobar') }}

Related

Slim 3 render method not valid

I want to make simple template rendering in Slim3 but I get an error:
Here is my code :
namespace controller;
class Hello
{
function __construct() {
// Instantiate the app
$settings = require __DIR__ . '/../../src/settings.php';
$this->app = new \Slim\App($settings);
}
public function index(){
return $this->app->render('web/pages/hello.phtml'); //LINE20
}
}
This is the error I get :
Message: Method render is not a valid method
The App object doesn't handle any rendering on its own, you'll need a template add-on for that, probably this one based on your template's .phtml extension. Install with composer:
composer require slim/php-view
Then your controller method will do something like this:
$view = new \Slim\Views\PhpRenderer('./web/pages');
return $view->render($response, '/hello.phtml');
You'll eventually want to put the renderer in the dependency injection container instead of creating a new instance in your controller method, but this should get you started.
I handle this by sticking my renderer in the container. Stick this in your main index.php file.
$container = new \Slim\Container($configuration);
$app = new \Slim\App($container);
$container['renderer'] = new \Slim\Views\PhpRenderer("./web/pages");
Then in your Hello class's file.
class Hello
{
protected $container;
public function __construct(\Slim\Container $container) {
$this->container = $container;
}
public function __invoke($request, $response, $args) {
return $this->container->renderer->render($response, '/hello.php', $args);
}
}
To clean up this code, make a base handler that has this render logic encapsulated for you.

Is there a way to display the original content of a twig template once it is loaded, before rendering anything?

Does someone know if there is a way to display the original raw content of a twig template once it is loaded, before rendering anything ?
Let's say that I've got a root template:
{# templates/template.txt.twig #}
This is my root template
{{arg1}}
{{ include('other/internal.txt.twig') }}
and another one included from the root one:
{# templates/other/internal.txt.twig #}
This is my included template
{{arg2}}
If I render template.txt.twig with arg1='foo' and arg2='bar', the result will be
This is my root template
foo
This is my included template
bar
It there a way to retrieve the loaded template before any variable evaluation ?
What I expect is to get something like this:
This is my root template
{{arg1}}
This is my included template
{{arg2}}
The idea behind that is to leverage all twig loading mechanism (loading path, namespaces...)
because I need to make some custom automatic checks on the twig code itself (not related to twig syntax but with the consistency of a high level model not related to twig)
Let me know if this makes sense or if you need more information
Thank you for your help
To render a template, Twig compiles the Twig content into PHP code so I don't think this is possible with Twig itself. You can have a look at the doc or at the presentation of Matthias Noback that Thierry gave you the link.
The only solution I think you have is to read the file with file_get_contents but you won't have the included template in the right place as in your exemple.
If you are wanting to process the template with some kind of service before rendering it then you could override the twig environment and run your checks in the loadTemplate method.
For example here I will inject a templateValidator and then run that on each template load.
App\AcmeBundle\Twig\Twig_Environment
First extend \Twig_Environment and override the loadTemplate method as well as add a setter for injecting the templateValidator.
In the loadTemplate I have replaced instances of $this->getLoader()->getSource($name) with $this->validateTemplate($name) which does the same thing but also before what ever action you wish to add (in this case $this->templateValidator->validate($source).
namespace App\AcmeBundle\Twig;
use \Twig_Environment as BaseEnvironment;
class Twig_Environment extends BaseEnvironment
{
protected $templateValidator;
public function setTemplateValidator(TemplateValidator $templateValidator)
{
$this->templateValidator = $templateValidator;
}
public function loadTemplate($name, $index = null)
{
$cls = $this->getTemplateClass($name, $index);
if (isset($this->loadedTemplates[$cls])) {
return $this->loadedTemplates[$cls];
}
if (!class_exists($cls, false)) {
if (false === $cache = $this->getCacheFilename($name)) {
//eval('?>'.$this->compileSource($this->getLoader()->getSource($name), $name));
eval('?>'.$this->compileSource($this->validateTemplate($name), $name));
} else {
if (!is_file($cache) || ($this->isAutoReload() && !$this->isTemplateFresh($name, filemtime($cache)))) {
//$this->writeCacheFile($cache, $this->compileSource($this->getLoader()->getSource($name), $name));
$this->writeCacheFile($cache, $this->compileSource($this->validateTemplate($name), $name));
}
require_once $cache;
}
}
if (!$this->runtimeInitialized) {
$this->initRuntime();
}
return $this->loadedTemplates[$cls] = new $cls($this);
}
/**
* Validate template and return source
*
* #param string $name
* #return string
*/
private function validateTemplate($name)
{
$source = $this->getLoader()->getSource($name);
if (null !== $this->templateValidator) {
$this->templateValidator->validate($source);
}
return $source;
}
}
app/config/config.yml
Override the parameter twig.class so DI uses your class rather than the original \Twig_Environment.
parameters:
twig.class: App\AcmeBundle\Twig\Twig_Environment
App\AcmeBundle\DependencyInject\Compiler\TwigEnvironmentInjectCompilerPass
Create a compiler class to inject the templateValidator into your newly created TwigEnvironment (only if it has the 'setTemplateValidator' method so it degrades properly)
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class TwigEnvironmentInjectCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('twig') ||
!$container->hasDefinition('acme.template_validator'))
{
return;
}
$twig = $container->getDefinition('twig');
if (!$twig->hasMethodCall('setTemplateValidator')) {
return;
}
$twig->addMethodCall(
'setTemplateValidator',
array(new Reference('acme.template_validator'))
);
}
}
App\AcmeBundle\AppAcmeBundle
Add your compiler pass to the bundle build
use App\AcmeBundle\DependencyInject\Compiler\TwigEnvironmentInjectCompilerPass;
class AppAcmeBundle extends Bundle
{
/**
* {#inheritdoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new TwigEnvironmentInjectCompilerPass());
}
}
Note This isn't tested in any way, it's all just off the top of my head, so it could all be wrong.
I believe the Elao/WebProfilerExtraBundle available at :
https://github.com/Elao/WebProfilerExtraBundle
might provide you with the additional details you're seeking in the aforementioned example.
See also:
http://qpleple.com/webprofilerbundleextra-a-must-have-tool-for-symfony2-developers/
http://www.slideshare.net/matthiasnoback/diving-deep-into-twig

Access model from Thread in Yii

I have to parse a huge csv files in a Yii 1.1 Application.
Each row has to be validated and saved to the database.
I decided to use Multi Threading for this task.
So here is my code in the Controller action:
public function parseData($) {
$this->content = explode("\n", $this->content);
$thread_1 = new DatalogThread(array_slice($this->content, 0, 7000));
$thread_2 = new DatalogThread(array_slice($this->content, 7001));
$thread_1->start();
$thread_2->start();
}
And the Thread (I put it in models folder):
class DatalogThread extends Thread {
public $content;
public function __construct($content) {
$this->content = $content;
}
public function run() {
foreach ($this->content as $value) {
$row = str_getcsv($value);
$datalog = new Datalog($row);
$datalog->save();
}
}
}
The problem is that the Thread does not get access to the model file:
Fatal error: Class 'Datalog' not found in C:\xampp...\protected\models\DatalogThread.php
I tried Yii::autoload("Datalog"), but got The following error:
Fatal error: Cannot access property Yii::$_coreClasses in ...\YiiMain\framework\YiiBase.php on line 402
Yii uses a LOT of statics, this is not the best kind of code for multi-threading.
What you want to do is initialize threads that are not aware of Yii and reload it, I do not use Yii, but here's some working out to give you an idea of what to do:
<?php
define ("MY_YII_PATH", "/usr/src/yii/framework/yii.php");
include (MY_YII_PATH);
class YiiThread extends Thread {
public $path;
public $config;
public function __construct($path, $config = array()) {
$this->path = $path;
$this->config = $config;
}
public function run() {
include (
$this->path);
/* create sub application here */
}
}
$t = new YiiThread(MY_YII_PATH);
$t->start(PTHREADS_INHERIT_NONE);
?>
This will work much better ... I should think you want what yii calls a console application in your threads, because you don't want it trying to send any headers or anything like that ...
That should get you started ...

Twig renders / display only the view path

I'm creating an mvc structure for learning/teaching purpouses and so far I could set up the structure and a controller plus twig as template system.
The structure is:
index.php
controllers/
error.php
inc/
controller_base.php
view_manager.php
views/
.cache/
error/
view.html
So:
index instantiate twig autoloader (and mvc autoloader by spl_register).
index instantiate error controller inheriting controller_base.
controller_base is holding the view_manager.
error call view_manager to display the error/view.html and the only thing I get on the browser is error/view.html.
No errors on the apache log. (error_reporting(E_ALL))
Twig cache files created correctly, but the content doesn't look good to me:
protected function doDisplay(array $context, array $blocks = array()) {
// line 1
echo "error/view.html";
}
Anyone knows why, and how can I print the actual view?
Thanks in advance.
Code:
index.php: Declaring autoloaders
function __autoload($class_name)
{
if(file_exists("controllers/$class_name.php")):
include strtolower("controllers/$class_name.php");
elseif(file_exists("models/$class_name.php")):
include strtolower("models/$class_name.php");
elseif(file_exists("inc/$class_name.php")):
include strtolower("inc/$class_name.php");
endif;
}
spl_autoload_register('__autoload');
require_once 'vendor/autoload.php';
Twig_Autoloader::register(); has been avoided cause Twig installation was done by composer.
Adding it doesn't bring any change.
error.php (controller): called method.
public function show($param)
{
$this->viewMng->display(get_class().$data['view'], array())
}
controller_base.php:
class base
{
protected $viewMng;
public function __construct()
{
$this->viewMng = new viewmanager();
}
}
viewmanager.php: whole class
class viewmanager {
private $twig;
protected $template_dir = 'views/';
protected $cache_dir = 'views/.cache';
// protected $vars = array();
public function __construct($template_dir = null) {
if ($template_dir !== null) {
// Check here whether this directory really exists
$this->template_dir = $template_dir;
}
$loader = new Twig_Loader_String($this->template_dir);
$this->twig = new Twig_Environment($loader, array(
'cache' => $this->cache_dir));
}
public function render($template_file, $data = array()) {
if (!file_exists($this->template_dir.$template_file)) {
throw new Exception('no template file ' . $template_file . ' present in directory ' . $this->template_dir);
}
return $this->twig->render($template_file, $data);
}
public function display($template_file, $data) {
if (!file_exists($this->template_dir.$template_file)) {
throw new Exception('no template file ' . $template_file . ' present in directory ' . $this->template_dir);
}
$tmpl = ($this->twig->loadTemplate($template_file));//print_r($tmpl);
$tmpl->display($data);
}
}
view.html:
<html><body> Hello </body></html>
The problem is based on the loader.
According to the Twig documentation:
Twig_Loader_String loads templates from strings. It's a dummy loader
as the template reference is the template source code This
loader should only be used for unit testing as it has severe
limitations: several tags, like extends or include do not make sense
to use as the reference to the template is the template source code
itself.
That's why it only prints the passed in string.
Twig_Loader_String, should be replaced by the proper loader.
In this case it works perfectly well Twig_Loader_Filesystem.
$loader = new Twig_Loader_Filesystem($this->template_dir);
This solve the problem and the MVC structure works completely fine.
Thanks taking a look, guys.

How to include a php and then remove it?

Well, I don't know if this post have the correct title. Feel free to change it.
Ok, this is my scenario:
pluginA.php
function info(){
return "Plugin A";
}
pluginB.php
function info(){
return "Plugin B";
}
Finally, I have a plugin manager that is in charge of import all plugins info to pool array:
Manager.php
class Manager
{
protected $pool;
public function loadPluginsInfo()
{
$plugin_names = array("pluginA.php", "pluginB.php");
foreach ($plugin_names as $name)
{
include_once $name;
$this->pool[] = info();
}
}
}
The problem here is that when I print pool array it only show me the info on the first plugin loaded. I supposed that the file inclusing override the info because it still calling the info() method from the first include.
Is there a way to include the info of both plugins having the info() function with the same name for all plugins files?
Thank you in advance
PS: a fatal cannot redeclare error is never hurled
you can use the dynamic way to create plugin classes
plugin class
class PluginA
{
public function info()
{
return 'info'; //the plugin info
}
}
manager class
class Manager
{
protected $pool;
public function loadPluginsInfo()
{
$plugin_names = array("pluginA", "pluginB"); //Plugin names
foreach ($plugin_names as $name)
{
$file = $name . '.php';
if(file_exists($file))
{
require_once($file); //please use require_once
$class = new $name(/* parameters ... */); //create new plugin object
//now you can call the info method like: $class->info();
}
}
}
}
Are you sure the interpreter isn't choking w/ a fatal error? It should be since you're trying to define the info function twice here.
There are many ways to achieve what you want, one way as in #David's comment above would be to use classes, eg.
class PluginA
{
function info() { return 'Plugin A'; }
}
class PluginB
{
function info() { return 'Plugin B'; }
}
then the Manager class would be something like this:
class Manager
{
protected $pool;
public function loadPluginsInfo()
{
$plugin_names = array("PluginA", "PluginB");
foreach ($plugin_names as $name)
{
include_once $name . '.php';
$this->pool[] = new $name();
}
}
}
Now you have an instance of each plugin class loaded, so to get the info for a plugin you would have $this->pool[0]->info(); for the first plugin. I would recommend going w/ an associative array though so you can easily reference a given plugin. To do this, the assignment to the pool would become:
$this->pool[$name] = new name();
And then you can say:
$this->pool['PluginA']->info();
for example.
There are many other ways to do it. Now that 5.3 is mainstream you could just as easily namespace your groups of functions, but I would still recommend the associative array for the pool as you can reference a plugin in constant time, rather than linear.

Categories