PHP
Article

Laravel Blade Recursive Partials with @each

By Bruno Skvorc

In this tutorial, we’ll go through the process of implementing recursive partials in Laravel’s Blade templating engine by means of the @each command. This will allow us to render data structures with an arbitrary number of nested children without needing to know the maximum depth of the array.

The Data

The data I’m talking about is data like folder structures which can go deep into many levels. For our case, let’s imagine we’re dealing with a predefined data set of “Projects” in a todo application like Todoist. Feel free to grab the sample data from this gist or the code embed below:

$a = array(
            0 => array(
                'indent' => 1,
                'name' => 'Inbox',
                'color' => '#dddddd',
                'is_deleted' => 0,
                'collapsed' => 0,
                'inbox_project' => true,
                'archived_date' => null,
                'item_order' => 0,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837507,
                'children' => array(),
                'parent' => 'root',
            ),
            1 => array(
                'indent' => 1,
                'name' => 'Personal',
                'color' => '#fc603c',
                'is_deleted' => 0,
                'collapsed' => 0,
                'archived_date' => null,
                'item_order' => 1,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837508,
                'children' => array(),
                'parent' => 'root',
            ),
            2 => array(
                'indent' => 1,
                'name' => 'Work',
                'color' => '#a8c9e5',
                'is_deleted' => 0,
                'collapsed' => 0,
                'archived_date' => null,
                'item_order' => 2,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837509,
                'children' => array(
                    0 => array(
                        'indent' => 2,
                        'name' => 'Work indent 1-1',
                        'color' => '#a8c9e5',
                        'is_deleted' => 0,
                        'collapsed' => 0,
                        'archived_date' => null,
                        'item_order' => 3,
                        'is_archived' => 0,
                        'archived_timestamp' => 0,
                        'user_id' => 3840103,
                        'id' => 139576614,
                        'children' => array(
                            0 => array(
                                'indent' => 3,
                                'name' => 'Work indent 1-2',
                                'color' => '#dddddd',
                                'is_deleted' => 0,
                                'collapsed' => 0,
                                'archived_date' => null,
                                'item_order' => 4,
                                'is_archived' => 0,
                                'archived_timestamp' => 0,
                                'user_id' => 3840103,
                                'id' => 139576626,
                                'children' => array(),
                                'parent' => 139576614,
                            ),
                            1 => array(
                                'indent' => 3,
                                'name' => 'Work indent 1-2 2nd',
                                'color' => '#dddddd',
                                'is_deleted' => 0,
                                'collapsed' => 0,
                                'archived_date' => null,
                                'item_order' => 5,
                                'is_archived' => 0,
                                'archived_timestamp' => 0,
                                'user_id' => 3840103,
                                'id' => 139576629,
                                'children' => array(),
                                'parent' => 139576614,
                            ),
                        ),
                        'parent' => 138837509,
                    ),
                    1 => array(
                        'indent' => 2,
                        'name' => 'Work indent 2-1',
                        'color' => '#a8c9e5',
                        'is_deleted' => 0,
                        'collapsed' => 0,
                        'archived_date' => null,
                        'item_order' => 6,
                        'is_archived' => 0,
                        'archived_timestamp' => 0,
                        'user_id' => 3840103,
                        'id' => 139576622,
                        'children' => array(
                            0 => array(
                                'indent' => 3,
                                'name' => 'Work indent 2-2',
                                'color' => '#dddddd',
                                'is_deleted' => 0,
                                'collapsed' => 0,
                                'archived_date' => null,
                                'item_order' => 7,
                                'is_archived' => 0,
                                'archived_timestamp' => 0,
                                'user_id' => 3840103,
                                'id' => 139576636,
                                'children' => array(),
                                'parent' => 139576622,
                            ),
                        ),
                        'parent' => 138837509,
                    ),
                    2 => array(
                        'indent' => 2,
                        'name' => 'Work indent 3-1',
                        'color' => '#dddddd',
                        'is_deleted' => 0,
                        'collapsed' => 0,
                        'archived_date' => null,
                        'item_order' => 8,
                        'is_archived' => 0,
                        'archived_timestamp' => 0,
                        'user_id' => 3840103,
                        'id' => 139576646,
                        'children' => array(),
                        'parent' => 138837509,
                    ),
                ),
                'parent' => 'root',
            ),
            3 => array(
                'indent' => 1,
                'name' => 'Errands',
                'color' => '#74e8d4',
                'is_deleted' => 0,
                'collapsed' => 0,
                'archived_date' => null,
                'item_order' => 9,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837510,
                'children' => array(),
                'parent' => 'root',
            ),
            4 => array(
                'indent' => 1,
                'name' => 'Shopping',
                'color' => '#dddddd',
                'is_deleted' => 0,
                'collapsed' => 0,
                'archived_date' => null,
                'item_order' => 10,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837511,
                'children' => array(),
                'parent' => 'root',
            ),
            5 => array(
                'indent' => 1,
                'name' => 'Movies to watch',
                'color' => '#e3a8e5',
                'is_deleted' => 0,
                'collapsed' => 0,
                'archived_date' => null,
                'item_order' => 11,
                'is_archived' => 0,
                'archived_timestamp' => 0,
                'user_id' => 3840103,
                'id' => 138837512,
                'children' => array(),
                'parent' => 'root',
            ),
        );

Plain old PHP

When using plain old PHP for outputting such data, one would probably use a method like this one:

public function output($projects)
    {
        $string = "<ul>";
        foreach ($projects as $i => $project) {
            $string .= "<li>";
            $string .= $project['name'];
            if (count($project['children'])) {
                $string .= $this->output($project['children']);
            }
            $string .= "</li>";
        }
        $string .= "</ul>";
        return $string;
    }

Ew. It works, but it’s highly inflexible and it mixes presentation with logic. Let’s not do this.

Blade Foreach

With Blade, things become a little simpler. We can use the foreach construct to help us out.

@if (count($projects) > 0)
    <ul>
    @foreach ($projects as $project)
        @include('partials.project', $project)
    @endforeach
    </ul>
@else
    @include('partials.projects-none')
@endif

As Blade doesn’t really support defining functions, thus not letting us call them recursively like the output function above, we need to define partials and have them call themselves:

  • partials/project.blade.php

    <li>{{ $project['name'] }}</li>
    	@if (count($project['children']) > 0)
    	    <ul>
    	    @foreach($project['children'] as $project)
    	        @include('partials.project', $project)
    	    @endforeach
    	    </ul>
    	@endif
  • partials/projects-none.blade.php

    You have no projects!

But… so much code for something so rudimentary. Is there no way to shorten this even further?

Blade @each

There is an un(der)documented feature of Laravel Blade that’ll help us decimate the LoC count in our template files, making the lives of both our devs and designers much easier. The feature is @each and is used thusly:

@each('viewfile-to-render', $data, 'variablename','optional-empty-viewfile')

The first argument is the template to render. This will usually be a partial, like our project.blade.php. The second one is the iterable dataset, in our case $projects. Third is the variable name the elements will use when being iterated upon. For example, in foreach ($data as $element), this argument would be element (without the $). The fourth argument is an optional one – it’s the name of the template file which should be rendered when the second argument ($data) is empty, i.e. has nothing to iterate over. If we apply all this to our case, we can replace this entire block:

@if (count($projects) > 0)
    <ul>
    @foreach ($projects as $project)
        @include('partials.project', $project)
    @endforeach
    </ul>
@else
    @include('partials.projects-none')
@endif

with

@each('partials.project', $projects, 'project', 'partials.projects-none')

Conclusion

In this short tutorial, we saw how we can leverage an underdocumented feature of Laravel Blade to drastically reduce the number of lines in our template code. By using @each and relying on partials and their ability to recursively call themselves, we have an amazing arsenal of tools at our disposal for outputting all manners of data – it’s just a matter of putting the building blocks in the right order.

You can use this approach of partials recursion to echo out directory trees, content management categories, employee directories, and much, much more.

Did you know about @each? Do you know of any other hidden gems? Let us know in the comments!

Comments
oliver

Interesting, but how would you deal with the "ul" tags?
I suppose they would be just dropped in your example, or is there a way to have a pre/post string/variable output by "@each"?

swader

Unfortunately, the partial doesn't lend itself to the each approach due to this, indeed. Extending this helper, though, is an option - perhaps an additional parameter like $wrappingElement would be useful.

themccallister

Is it possible to pass variables to the empty partial? Like so:

@each('_macros/document', $document, 'document', '_macros/document-empty', ['label' => 'Doc Label'])
Richard1

"...perhaps an additional parameter like $wrappingElement would be useful..."

This reminds me of Zend Frameworks form decorators and extra complexity needed when you want something slightly custom. Unfortunately, templating libraries like Blade add overhead where there wouldn't otherwise be in many cases.

Richard1

themccallister's question is a good illustration of the unnecessary overhead. You end up fiddling around with basic functionality instead of solving the problems you want to solve.

swader

It's not possible, no, not without tweaking the helper.

Yes, intimately familiar with that expressionless

themccallister

How is that unnecessary overhead? I get handed templates from designers and I put a lot of reuse on my partials to keep my code DRY, front-end and back-end. Especially like this project, when I have 204 date fields with custom JS that I would much rather extract to a partial template.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.