Search code examples
symfonytwig

How to use existing PHP header generation class function to populate a twig template


In the existing application, there is a function that dynamically generates the header using classes.

 use GLOBALNAMESPACE\Core\Header;

Then in the body, we use

<header>
  <title>My Page</title>
    <?php Header::setupHeader('[common],[reporthelper]') ?>
</header>

This would build all the headers like this when the page is rendered in the browser.

<header>
    <title>My Page</title> 

 <link rel="stylesheet" href="/openemr/public/assets/bootstrap/dist/css/bootstrap.min.css?v=44" type="text/css">
 <link rel="stylesheet" href="/openemr/public/assets/font-awesome/css/font-awesome.min.css?v=44" type="text/css">
 <link rel="stylesheet" href="/openemr/public/assets/jquery-datetimepicker/build/jquery.datetimepicker.min.css?v=44" type="text/css">

 <script type="text/javascript" src="/openemr/public/assets/jquery/dist/jquery.min.js?v=44"</script>

In my template file, I tried.

{%  
     include GLOBALNAMESPACE\\Core\\Header::setupHeader('[common],[reporthelper]')
 %}

This failed. Is there a way to include PHP classes?

UPDATE:

As suggested, created the HeaderExtension class in the twig directory.

Twig\HeaderExtension

Inside the file is this code:

 namespace Twig;

 use Twig\Extension\AbstractExtension;

class HeaderExtension extends AbstractExtension
{
public function getFunctions() {
    return [
        new TwigFunction(
            'header_setup',
            [OpenEMR\Core\Header::class, 'Header']
        ),
        // add more if needed
    ];
  }
}

In the method added

    $loader = new FilesystemLoader('../../templates/financialreports/insurance');
    $twig = new Environment($loader, [
        'cache' => 'C:\tempt',
    ]);

    return $twig->render('summaryinsurancepaid.html.twig', [
    'header' => new HeaderExtension(),
    'name' => 'Fabien Roger']);

Now, there are no error messages. However, the desired results are not achieved. There seems to be a call to the setupHeader() missing. In the new TwigFunction 'header_setup' is defined as a class called in the global namespace to the header. But there is nothing that calls the class method setupHeader() where I can add the array setupHeader(['common'],['reporthelper']). Passing in the array would bring back the desired results.

In the example on this page https://symfony.com/doc/current/templating/twig_extension.html.

They are using TwigFilter and not the TwigFunction to bring in a class at runtime. I changed this line, from this

 'header' => new HeaderExtension()

to this.

 'header' => new HeaderExtension('header_setup'),

No errors but no header either. So, I changed the template from this.

{{ header }}

To this:

{{ header(setupHeader(['common'],['reporthelper'])) }}

No error messages and no header either. End of update.


Solution

  • depending on the logic your Header does provide - especially regarding the output it creates - there are different approaches:

    expose select Header methods via twig functions

    namespace App\Twig;
    
    class HeaderExtension extends \Twig\Extension\AbstractExtension {
        public function getFunctions() {
            return [
                new \Twig\TwigFunction(
                    'header_setup', 
                    [GLOBALNAMESPACE\Core\Header::class, 'setupAssets']
                ),
                // add more if needed
            ];
        }
    }
    

    and then use it in your template as

    {{ header_setup('[common],[reporthelper]') }}
    

    expose Header's static functions via one twig function

    A slight workaround could be another approach almost the same TwigFunction as before, but using:

    new \Twig\TwigFunction('header', function($func, ...$args) {
        return call_user_func_array([GLOBALNAMESPACE\Core\Header::class, $func], $args);
    }
    

    which then would allow:

    {{ header('setupAssets', '[common],[reporthelper]') }}
    

    which obviously is a weirder syntax. you could go one step further

    expose all class/instance functions

    new \Twig\TwigFunction('call', function($class, $func, ...$args) {
        return call_user_func_array([$class, $func], $args);
    }
    

    and then use as:

    {{ call('GLOBALNAMESPACE\\Core\\Header', 'setupAssets', '[common],[reporthelper]') }}
    

    (which obviously poses an increased risk in security, if someone is able to edit templates, since now you can call all static functions anywhere...)

    rewrite Header into something more beautiful

    I assume some if not all of Header's functionality is quite basic and could be expressed as a twig template itself, like ...

    {% set targets = targets ?? ['common'] %}{# <-- defaults for every template? #}
    {% if 'common' in targets %}
     <link rel="stylesheet" href="/openemr/public/assets/bootstrap/dist/css/bootstrap.min.css?v=44" type="text/css">
     <link rel="stylesheet" href="/openemr/public/assets/font-awesome/css/font-awesome.min.css?v=44" type="text/css">
     <link rel="stylesheet" href="/openemr/public/assets/jquery-datetimepicker/build/jquery.datetimepicker.min.css?v=44" type="text/css">
    
     <script type="text/javascript" src="/openemr/public/assets/jquery/dist/jquery.min.js?v=44"></script> 
    {% endif %}
    {% if 'reporthelper' in targets %}
     <script type="text/javascript" src="/openemr/public/assets/reporthelper/..."></script>
    {% endif %}
    

    and so on. However, I'm quite sure if the functionality you're looking for may be available via webpack encore

    update: how to add a twig extension to your symfony project & use it

    assuming you're not just using symfony components (in which case the following will not apply), there are several ways how this is supposed to go:

    1. actually extending twig

    extending twig works in symfony by adding twig.extension to your service (i.e. your twig extension), so, in your services yaml (be careful about matching the indentation with spaces as needed):

    services:
        App\Twig\HeaderExtension:
            tags: ['twig.extension']
    

    if you don't want this extension to be globally available, you can use symfony's dependency injection to fetch the twig service and add the extension

    public function yourRouteAction(
        \Twig\Environment $twig
        /* your other parameters */
    ) {
        $twig->addExtension(new HeaderExtension());
        
        // don't *have* to use $twig here, symfony provides the very
        // same twig environment wherever it's referenced. this is not true
        // if you create one on the fly with new Twig...
        return $this->render(...); 
    

    just to be explicit about this: adding the extension provides the function in twig, you do NOT have to add it in the render-call at all!

    2. fixing your extension / usage

    Now that you actually added the extension as an extension and not just as a variable you have to manually add every time, there will be a function available:

    {{ header_setup(...) }}
    

    as I already wrote, you have to provide a callable as a second argument to the TwigFunction constructor. Since the last time you changed what you wanted to call, so I updated my answer. The structure of a callable is: [classname, functionname] for static calls, so in your case: [Header::class, 'setupAssets']. that means, your call in twig is then

    {{ header_setup(a) }} {{ header_setup(b,c,d) }} {{ header_setup([e,f]) }}
    

    which will translate into

    Header::setupAssets(a) // and
    Header::setupAssets(b,c,d) // and
    Header::setupAssets([e,f])
    

    respectively.

    this also assumes, that your Header::setupAssets() returns the text, so that it is inserted at the correct position. I'm not very certain how twigs echos stuff, but if your Header echos everything right away instead of returning it, which your code suggests, you might have to use output buffering to catch it:

            new \Twig\TwigFunction(
                'header_setup', 
                function(...$args) {
                    ob_start();
                    call_user_func_array(
                       [GLOBALNAMESPACE\Core\Header::class, 'setupAssets'],
                       $args
                    ); // if Header::setupAssets echoes directly, it will be caught
                    return ob_get_clean(); // returns caught stuff
                }
            ),