# Nested Modules

Out of the box, Twill supports 2 kinds of nested modules: self-nested and parent-child.

# Self-nested modules

Self-nested modules allow items to be nested within other items of the same module (e.g. Pages can contain other Pages):

self-nested module

# Creating self-nested modules

You can enable nesting when creating a new module with the --hasNesting or -N option:

php artisan twill:make:module -N pages

This will prefill some options and methods in your module's controller and use the supporting traits on your model and repository.

This feature requires the laravel-nestedset package, which can be installed via composer:

composer require kalnoy/nestedset

# Working with self-nested items

A few accessors and methods are available to work with nested item slugs:

// Get the combined slug for all ancestors of an item in the current locale:
$slug = $item->ancestorsSlug;

// for a specific locale:
$slug = $item->getAncestorsSlug($lang);

// Get the combined slug for an item including all ancestors:
$slug = $item->nestedSlug;

// for a specific locale:
$slug = $item->getNestedSlug($lang);

To include all ancestor slugs in the permalink of an item in the CMS, you can dynamically set the $permalinkBase property from the form() method of your module controller:

class PageController extends ModuleController
{
    //...

    protected function form($id, $item = null)
    {
        $item = $this->repository->getById($id, $this->formWith, $this->formWithCount);

        $this->permalinkBase = $item->ancestorsSlug;

        return parent::form($id, $item);
    }
}

To implement routing for nested items, you can combine the forNestedSlug() method from HandleNesting with a wildcard route parameter:

// file: routes/web.php

Route::get('{slug}', function ($slug) {
    $page = app(PageRepository::class)->forNestedSlug($slug);

    abort_unless($page, 404);

    return view('site.page', ['page' => $page]);
})->where('slug', '.*');

For more information on how to work with nested items in your application, you can refer to the laravel-nestedset package documentation (opens new window).

# Parent-child modules

Parent-child modules are 2 distinct modules, where items of the child module are attached to items of the parent module (e.g. Issues can contain Articles):

parent-child modules

Items of the child module can't be created independently.

# Creating parent-child modules

We'll use the slug and position features in this example but you can customize as needed:

php artisan twill:make:module issues -SP
php artisan twill:make:module issueArticles -SP

Add the issue_id foreign key to the child module's migration:

class CreateIssueArticlesTables extends Migration
{
    public function up()
    {
        Schema::create('issue_articles', function (Blueprint $table) {
            // ...
            $table->unsignedBigInteger('issue_id')->nullable();
            $table->foreign('issue_id')->references('id')->on('issues');
        });
        
        // ...
    }
}

Run the migrations:

php artisan migrate

Update the child model. Add the issue_id fillable and the relationship to the parent model:

class IssueArticle extends Model implements Sortable
{
    // ...

    protected $fillable = [
        // ...
        'issue_id',
    ];
    
    public function issue()
    {
        return $this->belongsTo(Issue::class);
    }
}

Update the parent model. Add the relationship to the child model:

class Issue extends Model implements Sortable
{
    // ...

    public function articles()
    {
        return $this->hasMany(IssueArticle::class);
    }
}

Update the child controller. Set the $moduleName and $modelName properties, then override the getParentModuleForeignKey() method:

class IssueArticleController extends BaseModuleController
{
    protected $moduleName = 'issues.articles';

    protected $modelName = 'IssueArticle';

    protected function getParentModuleForeignKey()
    {
        return 'issue_id';
    }
}

Update the parent controller. Set the $indexColumns property to include a new Articles column. This will be a link to the child module items, for each parent.

class IssueController extends BaseModuleController
{
    protected $moduleName = 'issues';

    protected $indexColumns = [
        'title' => [
            'title' => 'Title',
            'field' => 'title',
        ],
        'articles' => [
            'title' => 'Articles',
            'nested' => 'articles',
        ],
    ];
}

Add both modules to routes/admin.php:

Route::module('issues');
Route::module('issues.articles');

Add the parent module to config/twill-navigation.php:

return [
    'issues' => [
        'title' => 'Issues',
        'module' => true,
    ],
];

Then, rename and move the articles/ views folder inside of the parent issues/ folder:

resources/views/admin/
└── issues
    ├── articles
    │   └── form.blade.php
    └── form.blade.php
...

# Using breadcrumbs for easier navigation

In the child module controller, override the indexData() method to add the breadcrumbs to the index view:

class IssueArticleController extends BaseModuleController
{
    // ...

    protected function indexData($request)
    {
        $issue = app(IssueRepository::class)->getById(request('issue'));

        return [
            'breadcrumb' => [
                [
                    'label' => 'Issues',
                    'url' => moduleRoute('issues', '', 'index'),
                ],
                [
                    'label' => $issue->title,
                    'url' => moduleRoute('issues', '', 'edit', $issue->id),
                ],
                [
                    'label' => 'Articles',
                ],
            ],
        ];
    }
}

child module index


Then, override the formData() method to do the same in the form view:

    protected function formData($request)
    {
        $issue = app(IssueRepository::class)->getById(request('issue'));

        return [
            'breadcrumb' => [
                [
                    'label' => 'Issues',
                    'url' => moduleRoute('issues', '', 'index'),
                ],
                [
                    'label' => $issue->title,
                    'url' => moduleRoute('issues', '', 'edit', $issue->id),
                ],
                [
                    'label' => 'Articles',
                    'url' => moduleRoute('issues.articles', '', 'index'),
                ],
                [
                    'label' => 'Edit',
                ],
            ],
        ];
    }

nested child form