Best Practice / Resources for Blade

Having worked with this, mostly from @smutek’s examples, for a couple weeks now, it seems to me that the best use of Blade’s additional functionality is to provide context for templates.

The rabbit hole goes as deep as you want, since it’s just php, but the goal (in my mind) should be to remove complexity rather than to add it in all cases.

So for instance, with Sage 9’s implementation of Blade, let’s say you use the thumbnail size featured image on your archive template, but the medium size on your single. With the right directive you could call the_post_thumbnail($AllTheRightArguments) on both templates and let your directive set up the context based on which template is being used. This simplifies the template file itself and centralizes the context creation so it can be updated easily.

This is a round about way of saying that the test should be “does this simplify things, or am I inventing complexity?”. I fail this test two out of three times but I’m trying to get better :slight_smile:

2 Likes

Just chiming in to thank @smutek for the controller.php idea. Now my views are even cleaner!

:smiley: :smiley: :smiley:

Example:

src/controller.php

<?php

namespace App;

/**
 * Home
 */
function home()
{
    $data = [
        'steps' => [
            'Idea'    => 'icon-light-bulb',
            'Plan'    => 'icon-pencil',
            'Design'  => 'icon-laptop',
            'Develop' => 'icon-code',
            'Stage'   => 'icon-bug',
            'Deploy'  => 'icon-rocket'
        ]
    ];

    return $data;
}

add_filter('sage/template/home/data', 'App\\home');

?>

templates/partials/content-page-home.blade.php

<ol>
  @foreach ($steps as $title => $icon)
    <li>
      <div class="step">
        <i class="icon {{ $icon }}"></i>
        <span class="title">{{ $loop->iteration }}. {{ $title }}</span>
        @if ($loop->first or $loop->last)
          <span class="dot"></span>
        @endif
      </div>
      @if (!$loop->last)
        @if ($loop->iteration % 2 == 0)
          <i class="icon icon-arrow-long-up"></i>
        @else
          <i class="icon icon-arrow-long-down"></i>
        @endif
      @endif
    </li>
  @endforeach
</ol>

…now that is some clean markup! Also loving Blade’s auto-creation of the $loop variable.

Looking forward to living my new site with Sage 9. :slight_smile:

3 Likes

This is very neat. Where are you guys putting your controller.php file? With the specific template file as outlined in the filter name? Are using one controller for each page or one controller to rule all pages?

Also doesn’t the PSR-4 spec recommend \<NamespaceName>(\<SubNamespaceNames>)*\<ClassName> format? I’m still learning PSR-4 so I was just curious to why you weren’t approaching it that way rather than stay valid with the spec requirements.

Mine is simply in src/controller.php for now and then added to the array_map in functions.php:

['helpers', 'setup', 'filters', 'admin', 'controller']

I plan on pulling the data from my theme options panel so the above is just an example. I don’t plan on actually hardcoding the values there such as my array of steps.

As far as the namespaces, I was a little curious about that too. I’m just sticking with App for now to make it uniform with how the rest of Sage is setup. Maybe one of the devs can chime in and clear that up.

This is in reply to @christianmagill’s question up above…

Not 100% what you were asking, but perhaps Blade directives are what you’re looking for?

Here’s some quick and dirty examples to maybe spark some ideas from the rest of the community who may or may not know about these.

src/setup.php Line 150

/**
 * Create @posts Blade directive
 */
sage('blade')->compiler()->directive('posts', function () {
    return '<?php while(have_posts()) : the_post(); ?>';
});

/**
 * Create @endposts Blade directive
 */
sage('blade')->compiler()->directive('endposts', function () {
    return '<?php endwhile; ?>';
});

/**
 * Create @query() Blade directive
 */
sage('blade')->compiler()->directive('query', function ($args) {
    $output = '<?php $bladeQuery = new WP_Query($args); ?>';
    $output .= '<?php while ($bladeQuery->have_posts()) : ?>';
    $output .= '<?php $bladeQuery->the_post(); ?>';

    return $output;
});

/**
 * Create @endquery Blade directive
 */
sage('blade')->compiler()->directive('endquery', function () {
    return '<?php endwhile; ?>';
});

/**
 * Create @title Blade directive
 */
sage('blade')->compiler()->directive('title', function () {
    return '<?php the_title(); ?>';
});

/**
 * Create @content Blade directive
 */
sage('blade')->compiler()->directive('content', function () {
    return '<?php the_content(); ?>';
});

/**
 * Create @excerpt Blade directive
 */
sage('blade')->compiler()->directive('excerpt', function () {
    return '<?php the_excerpt(); ?>';
});

Example Usage:

Usage for @query()

@php
  $args = array(
    'author' => 1
  );
@endphp

@query($args)
  <h2>@title</h2>
  @excerpt
@endquery

Usage for @posts

@posts
  <h2>@title</h2>
  @content
@endposts

These were done very quickly as an example and I’m sure could be improved on for real world usage.

Looking forward to seeing if anyone comes up with some other nice directives. I created an issue on the tracker about them at some point but the Sage dev’s wanted to keep their side of the things as clean as possible, but that doesn’t mean a plugin isn’t out of the question with adding some clean, well written directives to help make views even more pleasant.

By default, Sage is equipped with the @asset() directive to help link to various assets within’ your views. This is specifically useful for when using yarn run build:production as it will add cache busting to all of your filenames, so simply hardlinking them will cause them to break.

I’m sure this subject will be touched on significantly as Sage 9 gets out of beta and proper documentation is released.

12 Likes

Regarding the PSR-4 spec, I’m not entirely sure how the guts of this work but I think that this is being handled by the Laravel components. Note that the files in sage\src\lib\Sage all use fully qualified namespaces.

Check out the Laravel docs on application structure - Application Structure - Laravel 5.0 - The PHP Framework For Web Artisans

Here’s the relevant bit -

The “meat” of your application lives in the app directory. By default, this directory is namespaced under App and is autoloaded by Composer using the PSR-4 autoloading standard.

Doing the same here. :slight_smile:

1 Like

@Log1x - Hey, these directives are great . Thanks for sharing them! I’d tried putting some together a couple weeks back but wasn’t making much progress.

:slight_smile:

The possibilities are endless, really.

https://laracasts.com/discuss/channels/laravel/useful-blade-directives has some great examples of non-wordpress related directives such as @set, @varDump, etc.

In their examples, simply replace Blade::directive with sage('blade')->compiler()->directive.

Also, I mentioned it in another thread, but thought I’d also include here that you can also share variables such as $GLOBALS to all of your blade views so you don’t have to call them manually.

Example

/**
 * Share variables to Blade views
 */
sage('blade')->share('options', $GLOBALS['options']);
sage('blade')->share('shortcodes', $GLOBALS['shortcode_tags']);

and just like that, $options and $shortcodes are now available in your views.

<ul>
  @foreach (array_keys($shortcodes) as $shortcode)
    <li>[{{ $shortcode }}]</li>
  @endforeach
</ul>

I found this especially useful when using an options framework such as Redux and them having all of the data stored in a global variable. It was obnoxious to have to global the variable every time I needed to use it. This is much more convenient. :wink:

8 Likes

This is really cool and like @Twansparant I’m seriously thinking of dumping Timber for a pure Blade setup in combination with directives and a data controller.

Is there a way to move the directives to a separate PHP file? I’m not sure how to reference the Sage compiler using the sage function in helpers.php. When adding directives.php to the array_map function in functions.php a fatal error occurs as Illuminate cannot find the sage.blade class. For some reason this isn’t the problem when adding the directives to setup.php.

What’s your directives.php look like so far? Are you using the App namespace?

If not, try using \App\sage('blade') instead.

You probably know this, but we can simplify this even further:

<?php

namespace App;

/**
 * Home
 */
add_filter('sage/template/home/data', function() {
    return [
        'steps' => [
            'Idea'    => 'icon-light-bulb',
            'Plan'    => 'icon-pencil',
            'Design'  => 'icon-laptop',
            'Develop' => 'icon-code',
            'Stage'   => 'icon-bug',
            'Deploy'  => 'icon-rocket'
        ]
    ]; 
});

?>
5 Likes

Knew that, but didn’t think of it. I will go that route instead. Thanks!

I’m using the namespace and using it without the namespace (so \App\sage('blade')) throws the same error unfortunately. This is what my directives.php looks like currently.

<?php

namespace App;

/**
 * Create @title Blade directive
 */
sage('blade')->compiler()->directive('title', function () {
    return '<?php the_title(); ?>';
});

The error is an uncaught ReflectionException:
Uncaught exception 'ReflectionException' with message 'Class sage.blade does not exist' in /Path/to/web/app/themes/sage/vendor/illuminate/container/Container.php:749

Gotcha.

Assuming adding:

use Roots\Sage\Template\Blade;
use Roots\Sage\Template\BladeProvider;

will do the trick.

If not, I will post a fix once I’m on my computer.

1 Like

I was messing with those imports as well (copy pasted the Blade ones from setup.php). Adding the following (or just the two you mentioned) doesn’t help, unfortunately:

use Roots\Sage\Config;
use Roots\Sage\Template\Blade;
use Roots\Sage\Template\BladeProvider;
1 Like
<?php

namespace App;

add_action('after_setup_theme', function () {
    /**
     * Create @title Blade directive
     */
    sage('blade')->compiler()->directive('title', function () {
        return '<?php the_title(); ?>';
    });
});

?>

Here you go. :slight_smile:

3 Likes

Thanks for the help, it’s working now! I feel rather stupid, because after reviewing setup.php more closely I noticed that Blade is also referenced in the after_setup_theme action using the sage('blade') function. :neutral_face:

Hah. Don’t worry, I overlooked it too at first.

Played around with ACF Pro for the first time. Here’s some directives I came up with:

/**
 * Advanced Custom Fields Blade directives
 */

/**
 * Create @fields() Blade directive
 */
sage('blade')->compiler()->directive('fields', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    $output = "<?php if (have_rows($expression)) : ?>";
    $output .= "<?php while (have_rows($expression)) : ?>";
    $output .= "<?php the_row(); ?>";
    return $output;
});

/**
 * Create @endFields Blade directive
 */
sage('blade')->compiler()->directive('endFields', function () {
    return "<?php endwhile; endif; ?>";
});

/**
 * Create @field() Blade directive
 */
sage('blade')->compiler()->directive('field', function ($expression) {
     $expression = strtr($expression, array('(' => '', ')' => ''));
     return "<?php the_field($expression); ?>";
});

/**
 * Create @getField() Blade directive
 */
sage('blade')->compiler()->directive('getField', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    return "<?php get_field($expression); ?>";
});

/**
 * Create @hasField() Blade directive
 */
sage('blade')->compiler()->directive('hasField', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    return "<?php if (get_field($expression)) : ?>";
});

/**
 * Create @endField Blade directive
 */
sage('blade')->compiler()->directive('endField', function () {
    return "<?php endif; ?>";
});

/**
 * Create @sub() Blade directive
 */
sage('blade')->compiler()->directive('sub', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    return "<?php the_sub_field($expression); ?>";
});

/**
 * Create @getSub() Blade directive
 */
sage('blade')->compiler()->directive('getSub', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    return "<?php get_sub_field($expression); ?>";
});

/**
 * Create @hasSub() Blade directive
 */
sage('blade')->compiler()->directive('hasSub', function ($expression) {
    $expression = strtr($expression, array('(' => '', ')' => ''));
    return "<?php if (get_sub_field($expression)) : ?>";
});

/**
 * Create @endSub Blade directive
 */
sage('blade')->compiler()->directive('endSub', function () {
    return "<?php endif; ?>";
});

@fields() Usage

<ul>
  @fields('links')
    @hasSub('title')
      <li>@sub('title')</li>
    @endSub

    @hasSub('url')
      <li>@sub('url')</li>
    @endSub
  @endFields
</ul>

@field() Usage

@hasField('test')
  @field('test')
@endField

Not shown in my example is @getSub() and @getField() allowing you to return the values instead of echoing them.

Shoutout to @Daniel_Willitzer for providing me with ACF Pro so I could create these.

Any suggestions/modifications are welcome. I made these in the last 15 minutes and this is the first time I’ve ever used ACF, so there may be a more effective way to do these.

One thing would be to combine @hasField and @endField to make it a bit easier in certain use cases. The main reason I separated them were for styling purposes (i.e. wrapping @field() in <li></li>– obviously you don’t want the markup appearing if it doesn’t exist…)

I also learned that you have to strip the parenthesis when passing an $expression to a Blade directive. Kind of strange… This makes array's a little hard to accomplish. For that, it looks like the common thing to do is pass an array with comma’s and then explode the $expression variable and using it inside of the directive such as $expression = explode(',', $expression). You can see a few examples here of people doing that.

It’s not pretty, but in this case where I’m only passing string’s I simply did:

$expression = strtr($expression, array('(' => '', ')' => ''));

That being said, I will probably refactor the other directives I posted a few posts up to accommodate this.

12 Likes