A CakePHP helper to handle tree structures.
By default, it uses the core TreeBehavior and MPTT (Modified Preorder Tree Traversal). But it sure can work with any tree like input as nested object or array structure.
It can work with both arrays and Entity objects. The latter should be preferred as you can then use all properties and getters on that object.
Include helper in your AppView class as
$this->loadHelper('Tools.Tree', [
...
]);
Then you can use it in your templates as
echo $this->Tree->generate($articles);
By default, just outputting the display name is usually not enough.
You want to create some Template/Element/tree_element.ctp
element instead:
echo $this->Tree->generate($articles, ['element' => 'tree_element']);
That template can then contain all normal template additions, including full helper access:
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Article|\Cake\Collection\CollectionInterface $data
* @var bool $activePathElement
*/
if (!$data->visible) { // You can do anything here depending on the record content
return;
}
$label = $data->title;
if ($activePathElement) {
$label .= ' (active)';
}
?>
<li>
<?php echo $this->Html->link($label, ['action' => 'view', $data->id]); ?>
</li>
So the current entity object is available as $data
variable inside this snippet.
- $data : object|object[]|array
- $parent : object|array|null
- $depth : int
- $hasChildren : int
- $numberOfDirectChildren : int
- $numberOfTotalChildren : int
- $firstChild : bool
- $lastChild : bool
- $hasVisibleChildren : bool
- $activePathElement : string
- $isSibling : bool
plus all config values.
Here the same keys are available on the first argument ($data
array). So the above $data
would actually be
$data['data']
and usually be the entity.
If you are passing entities, it helps to inline annotate in this case:
$closure = function(array $data) {
/** @var \Cake\ORM\Entity $entity */
$entity = $data['data'];
return h($entity->name) . ($data['activePathElement'] ? ' (active)' : '');
};
When using the TreeHelper for navigation structures, you would usually want to set the active path as class elements ("active")
on the <li>
elements.
You can do that by passing in the current path.
// Your controller fetches the navigation tree
$tree = $this->Table->find('threaded')->toArray();
// The current active element in the tree (/view/6)
$id = 6;
// We need to get the current path for this element
$nodes = $this->Table->find('path', ['for' => $id]);
$path = $nodes->extract('id')->toArray();
// In your view
$options = [
'autoPath' => [$current->lft, $current->rght],
'treePath' => $path,
'element' => 'tree', // src/Template/Element/tree.ctp
];
echo $this->Tree->generate($tree, $options);
The autoPath
setting passed using [lft, rght]
of your current element will auto-add "active" into your elements.
You can also just pass the current entity ('autoPath' => $current
) and it will extract lft and rght properties based on the config.
The treePath
is optional and needed for additional things like hiding unrelated siblings etc.
You can also use any custom data array for building a tree, as long as it contains children
elements per element:
$treeData = [
[
'name' => 'Foo',
'children' => [
[
'name' => 'Bar',
'children' => [
],
],
[
'name' => 'Baz',
'children' => [
[
'name' => 'Baz Child',
'children' => [
],
],
],
],
],
],
];
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Invalid Tree Structure: Array');
echo $this->Tree->generate($treeData);
will output
<ul>
<li>Foo
<ul>
<li>Bar</li>
<li>Baz
<ul>
<li>Baz Child</li>
</ul>
</li>
</ul>
</li>
</ul>
You can read some more tutorial like details in my blog post.