Out of the box, Twill supports 2 kinds of nested modules: self-nested and parent-child.
Self-nested modules allow items to be nested within other items of the same module (e.g. Pages can contain other Pages):
You can enable nesting when creating a new module with the --hasNesting
or -N
option:
1php 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.
A few accessors and methods are available to work with nested item slugs:
1// Get the combined slug for all ancestors of an item in the current locale: 2$slug = $item->ancestorsSlug; 3 4// for a specific locale: 5$slug = $item->getAncestorsSlug($lang); 6 7// Get the combined slug for an item including all ancestors: 8$slug = $item->nestedSlug; 9 10// for a specific locale:11$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:
1class PageController extends NestedModuleController 2{ 3 //... 4 5 protected function form(?int $id, TwillModelContract $item = null): array 6 { 7 $item = $this->repository->getById($id, $this->formWith, $this->formWithCount); 8 9 $this->permalinkBase = $item->ancestorsSlug;10 11 return parent::form($id, $item);12 }13}
To implement routing for nested items, you can combine the forNestedSlug()
method from HandleNesting
with a wildcard route parameter:
1// file: routes/web.php2 3Route::get('{slug}', function ($slug) {4 $page = app(PageRepository::class)->forNestedSlug($slug);5 6 abort_unless($page, 404);7 8 return view('site.page', ['page' => $page]);9})->where('slug', '.*');
For more information on how to work with nested items in your application, you can refer to the laravel-nestedset package documentation.
You can also define the maximum depth allowed for the module changing the following:
1protected $nestedItemsDepth = 1;
Note: a depth of 1 means parent and child.
By default only a parent item will be visible to the browser field. If you want to show child items when browsing for the module you can set $showOnlyParentItemsInBrowsers
to false:
1protected $showOnlyParentItemsInBrowsers = false; // default is true
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):
Items of the child module can't be created independently.
We'll use the slug
and position
features in this example but you can customize as needed:
1php artisan twill:make:module issues -SP2php artisan twill:make:module issueArticles -SP --parentModel=Issue
Add the issue_id
foreign key to the child module's migration:
1public function up()2{3 Schema::create('issue_articles', function (Blueprint $table) {4 // ...5 $table->foreignIdFor(Issue::class);6 });7 8 // ...9}
Run the migrations:
1php artisan migrate
Update the child model. Add the issue_id
fillable and the relationship to the parent model:
1class IssueArticle extends Model implements Sortable 2{ 3 // ... 4 5 protected $fillable = [ 6 // ... 7 'issue_id', 8 ]; 9 10 public function issue()11 {12 return $this->belongsTo(Issue::class);13 }14}
Update the parent model. Add the relationship to the child model:
1class Issue extends Model implements Sortable2{3 // ...4 5 public function articles()6 {7 return $this->hasMany(IssueArticle::class);8 }9}
Update the child controller. Set the $moduleName
and $modelName
properties, then override the getParentModuleForeignKey()
method:
1use A17\Twill\Services\Breadcrumbs\NestedBreadcrumbs; 2 3class IssueArticleController extends BaseModuleController 4{ 5 protected $moduleName = 'issues.articles'; 6 protected $modelName = 'IssueArticle'; 7 8 protected function setUpController(): void 9 {10 if (request('issue')) {11 $this->setBreadcrumbs(12 NestedBreadcrumbs::make()13 ->forParent(14 parentModule: 'issues',15 module: $this->moduleName,16 activeParentId: request('issue'),17 repository: \App\Repositories\IssueRepository::class18 )19 ->label('Article')20 );21 }22 }23}
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.
1 2use A17\Twill\Services\Listings\Columns\NestedData; 3use A17\Twill\Services\Listings\TableColumns; 4 5class IssueController extends BaseModuleController 6{ 7 protected $moduleName = 'issues'; 8 9 protected function additionalIndexTableColumns(): TableColumns10 {11 $table = parent::additionalIndexTableColumns();12 13 $table->add(14 NestedData::make()->field('articles')15 );16 17 return $table;18 }19}
Add both modules to routes/twill.php
:
1<?php2 3use A17\Twill\Facades\TwillRoutes;4 5TwillRoutes::module('issues');6TwillRoutes::module('issues.articles');
Add the parent module to AppServiceProvider.php
:
1<?php 2 3namespace App\Providers; 4 5use A17\Twill\Facades\TwillNavigation; 6use A17\Twill\View\Components\Navigation\NavigationLink; 7use Illuminate\Support\ServiceProvider; 8 9class AppServiceProvider extends ServiceProvider10{11 public function boot(): void12 {13 TwillNavigation::addLink(14 NavigationLink::make()->forModule('issues')15 );16 }17}
Only when using blade forms:
Rename and move the articles/
views folder inside of the parent issues/
folder:
1resources/views/twill/2└── issues3 ├── articles4 │ └── form.blade.php5 └── form.blade.php6...
Breadcrumbs are added via the $this->setBreadcrumbs
method, you can remove that line if you wish to not include the breadcrumbs.