Search code examples
laravellaravel-views

How to render Laravel tags inside html stored in s3


Good day to all.

As the question title says, I do have HTML template code stored in s3 with Laravel tags on it, lets say:

<html>
<head>
    <title>{{ $landing->title }}</title>
</head>
<body>
@foreach($landing->products as $product)
    <p>{{ $product->title }}</p>
@endforeach
</body>
</html>

Then I would like to render this as post-processed html with the Laravel tags replaced as if it were a normal blade template.

In my controller I do have this:

print_r( view('render', compact('template', 'landing'))->render() );

(I don't want to show in it the browser, just get the html code)

And in the render.blade.php I have:

{!! html_entity_decode($template->html()) !!}

But this will show me the code with the Laravel tags without proper replacement.

Any lights on this? Any help will be appreciated.


Solution

  • I see two possible solutions to this problem.

    1. Compile the blade yourself (Not Optimal IMO)

    You can make a helper function that will compile blade for you, given a string. (source)

    helpers.php (Or wherever you want to locate the function)

    function compile_blade($markup, $data = []) {
        $fs = new \Illuminate\Filesystem\Filesystem;
        $b = new \Illuminate\View\Compilers\BladeCompiler($fs, __DIR__);
        $src = $b->compileString($markup);
    
        $isPhp = false;
        if (substr( $src, 0, 5 ) === "<?php")
        {
            $isPhp = true;
            $src = substr($src, 5);
        }
        
        $tempFileName = tempnam("/tmp", "blade-compile");
        file_put_contents($tempFileName, $src);
    
        ob_start();
    
        extract($data);
    
        include $tempFileName;
        $out = ob_get_clean();
        if ($isPhp)
        {
            $out = '<?php'.$out;
        }
        
        return $out;
    }
    

    Then in your controller, you would pre-process the s3 blade for consumption in your render.blade.php file like:

        return view('render', [
            'template' => compile_blade($template, $landing),
            'landing' => $landing,
        ));
    

    I don't think this is the optimal solution since you end up creating files anyways.

    1. Create a new namespace for blade/html coming from s3.

    Firstly you need to create a folder in your project like ./storage/local/blade. Then you need to add a namespace for views in that folder like so:

    AppServiceProvider.php

    public function boot()
    {
        ...
    
        view()->addNamespace('s3', storage_path('/local/views');
        ...
    }
    

    Now to handle retrieving the markup from s3 (in your controller, or elsewhere) you would do something like:

        // Lets say the file on s3 is markup.blade.php
        $contents = Storage::disk('s3')->get('path/to/markup.blade.php')
        Storage::disk('local')->put(storage_path('local/views/markup.blade.php'), $contents);
    

    Now, if your render.blade.php is being used solely to render the markup on s3, you should just use the new namespaced view instead. You can use this in your controller like:

        return view('s3::markup', compact('landing'));
    

    It becomes a bit more tricky if you want to use the s3 markup in one of your other blade files. But can be done by extending blade as in this post.

    Blade::extend(function($view, $compiler)
    {
        $pattern = $compiler->createMatcher('includeNamespaced');
    
        $viewPath = realpath($compiler->getPath());
        $parts = explode(DIRECTORY_SEPARATOR, $viewPath);
        $viewsDirectoryIndex = array_search('views', $parts);
        $namespace = $parts[$viewsDirectoryIndex + 1];
    
        $php = '$1<?php ';
        $php .= 'if($__env->exists(\''.$namespace.'.\'.$2)){';
        $php .= 'echo $__env->make(\''.$namespace.'.\'.$2)->render();';
        $php .= '}';
        $php .= 'else {';
        $php .= 'echo $__env->make($2)->render();';
        $php .= '}';
        $php .= '?>';
    
        return preg_replace($pattern, $php, $view);
    });
    

    Now you would be able to @include a namespaced view in your blade files like:

        @includeNamespaced('s3/markup')
    

    The other reason I prefer solution 2, is that you can get some "caching" effect, if you look to see if the file already exists in local/views, before downloading from s3. Then you can create a scheduled job that deletes files in storage/local/views older than some time limit.