Best Practice / Resources for Blade

I was playing around a little tonight and was able to return data to my page template. I’d love some feedback on this if anyone has time.

Okay, so I have 2 pages, titled “post-1” and post-2", and both pages are using a custom page template. In this case I’m using the default custom page template that ships with Sage, and I’ve made a partial called content-page-custom.blade.php

I also have a file I’ve added to /src/ called controllers.php, in which I have the follwing -

namespace App;

function post1()
{
    $data = [
        'title' => 'List',
        'names' => [
            'Frank',
            'Bob',
            'Mary'
        ]
    ];

    return $data;
}

add_filter( 'sage/template/post-1/data', 'App\\post1' );

function post2()
{
    $data = [
        'title' => 'List 2',
        'names' => [
            'Bill',
            'Sally',
            'Becky'
        ]
    ];

    return $data;
}

add_filter( 'sage/template/post-2/data', 'App\\post2' );

function both()
{
    $data = ['both' => 'Both'];
    // Undefined, gets overwritten
    return $data;
}

add_filter( 'sage/template/page-template-template-custom-blade/data', 'App\\both' );

Then, on my partial I have -

<h2>{{ $title or 'Not Defined' }}</h2>

<ul>
  @foreach($names as $name)
    <li>{{$name}}</li>
  @endforeach
</ul>

<h2>{{ $both or 'Not defined' }}</h2>

The lists output as expected, with post-1 getting the correct data back for the post-1 filter, and post 2 getting the correct data back as well.

The both() function should return identical data to both pages, and is hooked into page-template-template-custom-blade, but gets overwritten because it fires earlier and $data it’s in the same scope as the lists.

So… questions… :slight_smile:

Am I on the right track here?
What’s the best way to avoid having my variables overwritten, like in my both() example?
It seems that whatever I save in the $data variable has to be saved in key value pairs, right?

Any feedback would be most appreciated, I feel like I’m pretty close to understanding this.

3 Likes

Keep in mind that filter is a standard WP filter, so just like any other filter, you need to accept the original data/variable, modify it, and return it.

function both($data) {
    $data['both'] = 'Both';

    return $data;
}

Other than that, it looks good. That is basically what I had in mind if I was going to implement some simple controllers in Sage 9

3 Likes

Got it, thank you Kalen!

Wow, this is super nice man, fantastic job!

Here’s a simple image slider with Slick. This is using ACF’s gallery field, but anything will do.

Controller -

function slider( $data ) {

    $images = get_field('images');

    if ( $images ) {
        $data['images'] = $images;
    }

    return $data;
}
add_filter( 'sage/template/home/data', 'App\\slider' );

View -

<ul class="list-unstyled slider">
  @forelse($images as $image)
    <li><img src="{{$image['url']}}" alt="{{$image['alt']}}"></li>
  @empty
    <li class="alert alert-danger">No Images</li>
  @endforelse
</ul>

I’m giddy about this, it’s like a whole new WordPress!!! :sunny: :smile: :sunny:

14 Likes

You can even split it out and put the slider in it’s own little blade file, and have something like this -

@php(the_content())

@if($images)
  @include('partials/slider')
@endif

Sorry for all the posts, but this is so damn nice!

3 Likes

Interesting stuff indeed! I might ditch Timber for this after all :slight_smile:

I was just wundering how you guys would declare global data, regardless of the body class?
Stuff like menu’s, languages, ACF options etc?

Hey, check this one out for navs –

/**
 * Navigation arguments
 *
 * @param $data
 *
 * @return mixed
 */
function navControl( $data ) {

    // Pass the walker class to a var, so it
    // doesn't instantiate here.
    $bootstrapWalker = 'wp_bootstrap_navwalker';

    // Main Nav
    $mainNavArgs = [
        'theme_location' => 'primary_navigation',
        'walker'         => new $bootstrapWalker,
        'menu_class'     => 'navbar-nav mr-auto'
    ];

    $data['mainNavArgs'] = $mainNavArgs;

    // Social Nav
    $socialNavArgs = [
        'walker'         => new $bootstrapWalker,
        'theme_location' => 'social_navigation',
        'menu_class'     => 'nav social-nav',
        'link_before'    => '<span class="sr-only">',
        'link_after'     => '</span>'
    ];

    $data['socialNavArgs'] = $socialNavArgs;


    return $data;

}

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

The project I’m working on now has 2 navs, this changes the nav calls in my template file from this -

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    @if (has_nav_menu('primary_navigation'))
      {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'walker' => new wp_bootstrap_navwalker(), 'menu_class' => 'navbar-nav mr-auto']) !!}
    @endif
  </div>

to this -

<div class="collapse navbar-collapse" id="navbarSupportedContent">
    @if (has_nav_menu('primary_navigation'))
      {!! wp_nav_menu($mainNavArgs) !!}
    @endif
</div>

:slight_smile: Much cleaner!

I just added a body class called global so I can access it everywhere.

3 Likes

Man I am struggling to understand this, but I think I get it. This keeps the ugly (and duplicative if you’re calling it twice on a page) wp_nav_menu() arguments out of the template file, making everything easier to read. Then passes that args array to the template for use in $mainNavArgs.

Sort of like globaling the args in functions.php but not horrifying.

Is that close at least?

That’s the benefit for me, with the nav. It makes the markup cleaner and helps separate concerns a little more.

This is still new to me as well, maybe the nav example is overkill, but I’m almost certain to get obsessive and go overboard a little before I find some balance. :slight_smile:

OK so I implemented your Nav directive just to get comfortable with it. It works!

It seems like this could be a really easy way to go overboard and over-design a system, though. Instead of calling a WP function, I’m calling a function that calls that function. Is that better?

For me I guess it’s going to come down to moving logic outside of template files and also readability. For example, the social nav in my project would look like this -

{!! wp_nav_menu([ 'walker' => new bootstrapWalker, 'theme_location' => 'social_navigation', 'menu_class' => 'nav social-nav', 'link_before' => '<span class="sr-only">', 'link_after' => '</span>']) !!}

So when I look at that I feel like, man that’s kind of unruly, and I can simplify that to -

{!! wp_nav_menu($socialNavArgs) !!}

Which is much more readable to me and seems a good use.

I guess it could go a step further and move the nav menu call out as well, but personally I don’t mind the function call, so long as it’s paired down to a simple, meaningful expression when used inside a template file.

With this the intent is clear - give me the social nav:

{!! wp_nav_menu($socialNavArgs) !!}

I couldn’t see myself abstracting something like the_post() out of a template file for the exact reasons you mentioned. It doesn’t really make sense and seems like adding unnecessary complexity.

Does that make sense? Personally I’m stoked to be able to get logic out of my template files, and make template files more readable.

1 Like

I’m really liking the ideas in this thread!

Is there any way to get the results of the standard WP_Query for a view into a variable? I’d rather loop through posts with standard blade syntax.

I know I can requery entirely with get_posts() but I’m wondering if there’s a way to get a similar data return for the default query on a view.

Has anyone played around with this? How deep of a rabbit hole is it to go down?

Thanks,

Would love to add Blade SVG to my next Sage websites but I might have to read and get more comfortable with Blade.

Anyone knows if this is possible to begin with?

Edit: I guess this one is really tied to Laravel so it won’t work.
There’s always https://packagist.org/packages/oscarotero/inline-svg which works really well.

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