Installation:
composer require awaresoft/tree-bundle
Note: Follow the README for local development setup if modifying the bundle.
Enable the Bundle:
Add to config/bundles.php:
return [
// ...
Awaresoft\TreeBundle\AwaresoftTreeBundle::class => ['all' => true],
];
First Use Case:
Create a tree-aware entity (e.g., Category):
use Awaresoft\TreeBundle\Model\TreeInterface;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Category implements TreeInterface
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?string $name = null;
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
private ?self $parent = null;
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
private Collection $children;
// Implement TreeInterface methods (getParent(), getChildren(), etc.)
}
Register Admin (Sonata Integration):
Extend SonataAdmin and configure tree behavior in services.yaml:
services:
App\Admin\CategoryAdmin:
arguments: [~, App\Entity\Category, Awaresoft\TreeBundle\Admin\TreeAdminExtension]
tags: [sonata.admin]
Run Migrations:
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
TreeInterface for all tree-aware entities.class ProductCategory implements TreeInterface
{
// ... (implement TreeInterface)
public function getParent(): ?self { /* ... */ }
public function getChildren(): Collection { /* ... */ }
public function getRoot(): ?self { /* ... */ }
public function getLevel(): int { /* ... */ }
}
Awaresoft\TreeBundle\Model\TreeTrait to reduce boilerplate.TreeAdminExtension in your admin class.use Awaresoft\TreeBundle\Admin\TreeAdminExtension;
class CategoryAdmin extends AbstractAdmin
{
protected function configureListFields(ListMapper $listMapper): void
{
$listMapper
->add('name')
->add('parent.name', 'string', ['template' => 'AwaresoftTreeBundle:Admin:field_tree.html.twig'])
->add('_action', 'actions', [
'actions' => [
'edit' => [],
'delete' => [],
],
]);
}
public function getTemplate($name): string
{
if ($name === 'layout') {
return '@AwaresoftTreeBundle/Admin/layout.html.twig';
}
return parent::getTemplate($name);
}
}
TreeManager for common tree operations.$treeManager = $this->get('awaresoft_tree.manager.tree');
$rootNodes = $treeManager->getRootNodes(Category::class);
$subtree = $treeManager->getSubtree($rootNode);
TreeManager or create custom services.// services.yaml
App\Service\CustomTreeService:
arguments: ['@awaresoft_tree.manager.tree']
tags: [awaresoft_tree.tree_manager]
Doctrine Events:
Use prePersist, preUpdate, and preRemove to maintain tree integrity:
#[ORM\PrePersist]
#[ORM\PreUpdate]
public function updateLevel(): void
{
if ($this->parent) {
$this->level = $this->parent->level + 1;
} else {
$this->level = 0;
}
}
Twig Templates:
Override default templates in templates/AwaresoftTreeBundle/:
{# templates/AwaresoftTreeBundle/Admin/field_tree.html.twig #}
{% extends '@AwaresoftTreeBundle/Admin/field_tree.html.twig' %}
{% block field_tree %}
{{ parent() }}
<small>Custom suffix: {{ object.level }}</small>
{% endblock %}
API Responses: Serialize trees with custom DTOs:
class CategoryDTO
{
public function __construct(
private string $id,
private string $name,
private ?array $children = []
) {}
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'children' => array_map(fn($child) => (new self(...))->toArray(), $this->children),
];
}
}
Circular References:
inversedBy or mappedBy in Doctrine relations can cause infinite loops.#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
private ?self $parent = null;
#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
private Collection $children;
Level Calculation:
level updates can break if not handled in Doctrine events.TreeTrait or override lifecycle callbacks:
use Awaresoft\TreeBundle\Model\TreeTrait;
class Category implements TreeInterface
{
use TreeTrait;
// ...
}
Sonata Admin Caching:
php bin/console sonata:cache:clear
Performance with Large Trees:
TreeManager::getSubtree() with depth limits or lazy-loading:
$subtree = $treeManager->getSubtree($rootNode, 2); // Limit to 2 levels
Tree Structure Validation:
Use TreeManager to debug:
$treeManager = $this->get('awaresoft_tree.manager.tree');
$root = $treeManager->getRootNodes(Category::class)->first();
$subtree = $treeManager->getSubtree($root);
dump($subtree); // Inspect structure
Doctrine Query Logging: Enable SQL logging to check tree queries:
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
Symlink Issues:
ln -s /path/to/project/src/Awaresoft /path/to/vendor/awaresoft/tree-bundle/src/Awaresoft
php bin/console cache:clear
Custom Tree Managers:
Extend Awaresoft\TreeBundle\Manager\TreeManager for custom logic:
class CustomTreeManager extends TreeManager
{
public function getCustomSubtree(TreeInterface $node): array
{
// Custom logic
return $this->getSubtree($node);
}
}
Register in services.yaml:
services:
App\Manager\CustomTreeManager:
parent: awaresoft_tree.manager.tree
arguments: ['@doctrine.orm.entity_manager']
Event Subscribers:
Listen to tree events (e.g., TreeNodeMovedEvent):
use Awaresoft\TreeBundle\Event\TreeNodeMovedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class TreeEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
TreeNodeMovedEvent::NAME => 'onTreeNodeMoved',
];
}
public function onTreeNodeMoved(TreeNodeMovedEvent $event): void
{
// Custom logic (e
How can I help you explore Laravel packages today?