girover / tree
Building family tree
Requires
- php: ^8.0
- illuminate/contracts: ^8.0|^9.0
- spatie/laravel-package-tools: ^1.4.3
Requires (Dev)
- brianium/paratest: ^6.2
- nunomaduro/collision: ^5.3
- orchestra/testbench: ^6.15
- phpunit/phpunit: ^9.3
- spatie/laravel-ray: ^1.23
- vimeo/psalm: ^4.8
README
Building Family Tree
Content
- Introduction
- prerequisites
- Installation
- Assets
- Images
- Usage
- Testing
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Introduction
girover/laravel-family-tree is a package that allows you to build family trees. With this package it will be very simple to create trees and add nodes to trees.
Every tree is allowed to have 85 generations. Assuming each generation has 20 years, this means that 1700 years of data can be stored in one tree.
Every node in a tree is allowed to have 676 direct children.
prerequisites
- Laravel 8+
- PHP 8+
- Mysql 5.7+
Installation
You can add the package via composer:
composer require girover/laravel-family-tree
Before installing the package you should configure your database.
Then you can install the package by running Artisan command
php artisan tree:install
this command will take care of the following tasks:
- Publishing Config file
config\tree.php
to the config folder of your Laravel application. - Publishing migration files to folder
Database\migrations
in your application. - Migrate the published migrations.
- Publishing Assets (css, js, images) to the public folder in your application and they will be placed in
public\vendor\tree
. - Publishing JSON Translation files to the
resources\lang\vendor\tree
Assets
After publishing assets (css, js, images), they will be placed in public
folder
of the project in a folder called vendor/tree
.
You are free to move any of these assets to other directories.
You should add the CSS file vendor/tree/css/tree.css
to your blade file to get the tree styled.
Images
Every node in tree has an avatar photo. Male and Female Icons by default will be stored in the public folder under
vendor/tree/images
.
however you are free to choose another folder for the images of nodes, but
you must provide it in the config\tree.php
file.
'photos_folder' => 'path/to/your/images',
So your images folder should be in the public
folder.
Example: if images are stored in folder called images/avatars
the configs must be:
'photos_folder' => 'images/avatars',
Note: When saving photos in storage folder, it is important to create symbolic link to the photos folder.
Usage
To start building family trees, you must have two models. The first one represents trees in database and it must use the trait:
\Girover\Tree\Traits\Treeable
The second model represents nodes in these trees, and it must use trait:
\Girover\Tree\Traits\nodeable
NOTE: The names of the models that represents trees and nodes in database must be provided in config/tree.php
.
// config/tree.php return [ /* |---------------------------------------------------------- | Model That uses trait \Girover\Tree\Traits\Treeable |---------------------------------------------------------- | | example: App\Models\Family::class */ 'treeable_model' => App\Models\Family::class, /* |---------------------------------------------------------- | Model That uses trait \Girover\Tree\Traits\Nodeable |---------------------------------------------------------- | | example: App\Models\Person::class */ 'nodeable_model' => App\Models\Person::class, . . .
Treeable Model
To start building a tree or creating a new tree, it is very simple and thanks to Eloquent models from Laravel.
The model that represents trees in database should use trait: \Girover\Tree\Traits\Treeable
For example if your model is called Tree
so it should looks like this:
namespace App\Models; use Girover\Tree\Traits\Treeable; class Tree extends Model { use Treeable; }
And now you can use your model to deal with trees.
use App\Models\Tree; $tree = Tree::create( [ 'info' => 'info', 'another_info' => 'another info', ] );
After creating the tree, you can start to add as many nodes as you want.
Let's start adding the First node, the Root
, to the tree.
$data = ['name'=>'root', 'birth_date'=>'2000-01-01']; $tree->createRoot($data);
What if you want to make a new Root?
In this case you can use the method newRoot
, and the previously created Root will become a child of the new created Root.
$new_root_data = ['name'=>'new_root', 'birth_date'=>'2001-01-01']; $tree->newRoot($new_root_data);
After creating the Root in the tree, let's add first child for the Root.
use Girover\Tree\Models\Tree; $tree = Tree::find(1); $first_child_data = ['name'=>'first child', 'birth_date'=>'2001-01-01']; $tree->pointerToRoot()->newChild($first_child_data ,'m'); // m = male // Or you can do this instead $tree->pointerToRoot()->newSon($first_child_data);
Now our tree consists of two nodes, the root and the first child of the root.
You can call the following method on an object of Tree
Pointer
A tree has a Pointer inside it, and this Pointer points to one node.
Pointer can move through all nodes in the tree.
Because the Pointer points to a node inside the tree, so it can call all methods of model that uses Nodeable Trait .
To get the pointer you can do the following:
use App\Models\Tree; $tree = Tree::find(1); $pointer = $tree->pointer();
And now you can use the pointer to make a lot of actions inside the tree, for example moving through nodes, deleting and retrieving more information about nodes.
Eg.
To move the pointer to specific node:
use App\Models\Tree; use App\Models\Node; $tree = Tree::find(1); $node = Node::find(10); $tree->pointer()->to($node);
And now you can get the node data by calling the method node
$node = $pointer->node(); echo $node->attribute_1; echo $node->attribute_2; echo $node->attribute_3;
Note that we called method node
after we had called the method to($node)
.
This is because when a tree object is created, its Pointer indicates to null
.
Nodeable Model
- What is a node
- Retrieving nodes
- Adding nodes
- Deleting nodes
- Checking nodes
- Relationships
- Relocating nodes
- Attaching and Detaching nodes
What is a node
Node is a person in a tree and every node in the tree is connected with another one by using Location mechanism.
To retrieve a nodes you can use Eloquent model that uses trait Girover\Tree\Traits\Nodeable
use App\Models\Node; $node = Node::find(1);
Retrieving nodes
To get the tree that the node belongs to.
$node->getTree();
To get the father of the node:
return $node->father();
To get the grandfather of the node:
return $node->grandfather();
To get the ancestor that matches the given number as parameter.
$node->ancestor(); // returns father $node->ancestor(2); // returns grandfather $node->ancestor(3); // returns the father of grandfather
To get all ancestors of a node
return $node->ancestors();
To get all uncles of the node:
return $node->uncles();
To get all aunts of the node:
return $node->aunts();
To get all children of the node:
return $node->children();
To get all sons of the node:
return $node->sons();
To get all daughters of the node:
return $node->daughters();
To get all descendants of the node:
return $node->descendants();
To get all male descendants of the node:
return $node->maleDescendants();
To get all female descendants of the node:
return $node->femaleDescendants();
To get the first child of the node:
return $node->firstChild();
To get the last child of the node:
return $node->lastChild();
To get all siblings of the node:
return $node->siblings();
To get all brothers of the node:
return $node->brothers();
To get all sisters of the node:
return $node->sisters();
To get the next sibling of the node. gets only one sibling.
return $node->nextSibling();
To get all the next siblings of the node. siblings who are younger.
return $node->nextSiblings();
To get the next brother of the node. gets only one brother.
return $node->nextBrother();
To get all the next brothers of the node. brothers who are younger.
return $node->nextBrothers();
To get the next sister of the node. gets only one sister.
return $node->nextSister();
To get all the next sisters of the node. sisters who are younger.
return $node->nextSisters();
To get the previous sibling of the node. only one sibling.
return $node->prevSibling();
To get all the previous siblings of the node. siblings who are older.
return $node->prevSiblings();
To get the previous brother of the node. only one brother.
return $node->prevBrother();
To get all the previous brothers of the node. brothers who are older.
return $node->prevBrothers();
To get the previous sister of the node. only one sister.
return $node->prevSister();
To get all the previous sisters of the node. sisters who are older.
return $node->prevSisters();
To get the first sibling of the node.
return $node->firstSibling();
To get the last sibling of the node.
return $node->lastSibling();
To get the first brother of the node.
return $node->firstBrother();
To get the last brother of the node.
return $node->lastBrother();
To get the first sister of the node.
return $node->firstSister();
To get the last sister of the node.
return $node->lastSister();
Adding nodes
The next code will create father for a node, only if this node is a Root in the tree.
If the node is not a root
, Girover\Tree\Exceptions\TreeException
will be thrown.
$data = ['name' => $name, 'birth_date' => $birth_date]; $node->createFather($data); // OR $tree = Tree::find(1); $tree->pointerToRoot()->createFather($data);
To create new sibling for the node:
$data = ['name'=>$name, 'birth_date'=>$birth_date]; return $node->newSibling($data, 'm'); // m = male
To create new brother for the node:
$data = ['name'=>$name, 'birth_date'=>$birth_date]; $node->newBrother($data); // or you can use the newSibling method $node->newSibling($data, 'm');
To create new sister for the node:
$data = ['name'=>$name, 'birth_date'=>$birth_date]; $node->newSister($data); // or you can use the newSibling method $node->newSibling($data, 'f');
To create new son for the node:
$data = ['name'=>$name, 'birth_date'=>$birth_date]; $node->newSon($data); // or you can use the newChild method $node->newChild($data, 'm');
To create new daughter for the node:
$data = ['name'=>$name, 'birth_date'=>$birth_date]; $node->newDaughter($data); // or you can use the newChild method $node->newChild($data, 'f');
To make the node as the main node in its tree.
$node->makeAsMainNode();
Deleting nodes
To delete a node.
use App\Models\Person; $node = Person::find(10)->delete();
note: The node will be deleted with all its descendants.
But if you want just to delete the node and not its descendants,
then you can move its descendants before deleting the node.
use App\Models\Person; $node = Person::find(10); $another_node = Person::find(30); $node->moveChildrenTo($another_node); $node->delete();
Or you can directly move children to the father of deleted node by
using onDeleteMoveChildren
method without passing any node as parameter.
use App\Models\Person; $node = Person::find(10); $node->onDeleteMoveChildren()->delete();
To delete all children of a node:
use App\Models\Person; $node = Person::find(10); return $node->deleteChildren(); // number of deleted nodes
Checking nodes
To determine if the node is root in the tree
return $node->isRoot(); // returns true or false
To determine if the node is ancestor of another node:
use App\Models\Person; $node = Person::find(1); $another_node = Person::find(2); return $node->isAncestorOf($another_node); // returns true OR false
To determine if the node is father of another node:
use App\Models\Person; $node = Person::find(1); $another_node = Person::find(2); return $node->isFatherOf($another_node); // returns true OR false
Determine if the node has children
return $node->hasChildren(); // returns true or false
To determine if the node is child of another node:
use App\Models\Person; $node = Person::find(1); $another_node = Person::find(2); return $node->isChildOf($another_node); // returns true OR false
Determine if the node has siblings
return $node->hasSiblings(); // returns true or false
To determine if the node is sibling of another node:
use App\Models\Person; $node = Person::find(1); $another_node = Person::find(2); return $node->isSiblingOf($another_node); // returns true OR false
To count the children of the node:
return $node->countChildren();
To count sons of a node:
return $node->countSons();
To count daughters of the node:
return $node->countDaughters();
To count all siblings of the node:
return $node->countSiblings();
To count all brothers of the node:
return $node->countBrothers();
To count all sisters of the node:
return $node->countSisters();
To count all descendants of the node:
return $node->countDescendants();
To count all male descendants of the node:
return $node->countMaleDescendants();
To count all female descendants of the node:
return $node->countFemaleDescendants();
Relationships
To get all wives of the node:
$node->wives; // to add constraints $node->wives()->where('name', $name)->get();
When trying to get wives of female node a Girover\Tree\Exceptions\TreeException
will be thrown.
Note that this will get divorced wives too.
To get only wives who are not divorced you can do this:
$node->wives()->ignoreDivorced()->get();
To get husbands of the node:
$node->husband; // to add constraints $node->husband()->where('name', $name)->get();
When trying to get husband of male node a Girover\Tree\Exceptions\TreeException
will be thrown.
Note that this will get divorced husbands too.
To get only husbands who are not divorced you can do this:
$node->husband()->ignoreDivorced()->get();
To assign wife to a node:
$wife = Node::find($female_node_id) $data = ['date_of_marriage'=>'2000/01/01', 'marriage_desc'=>'description']; return $node->getMarriedWith($wife, $data);
When trying to do this with a female node (woman) a Girover\Tree\Exceptions\TreeException
will be thrown. so if $node
is a woman Exception will be thrown.
To divorce a wife
$husband = Node::find($male_node_id) $wife = Node::find($female_node_id) return $husband->divorce($wife);
When trying to do this with a female node a Girover\Tree\Exceptions\TreeException
will be thrown. so if $husband
is a woman Exception will be thrown.
To set uploaded photo to the node:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Node; class PersonController extends Controller { public function addPhoto(Request $request) { $person = Node::find($request->person_id); $photo = $request->file('photo'); $person->setPhoto($photo, 'new name'); return view('persons.index')->with('success', 'photo was added'); } }
Relocating nodes
to move an existing node to be child of another node.
Note this will move the node with its children.
use App\Models\Node; $node = Node::find(10); $another_node = Node::find(30); $node->moveTo($another_node);
To move children of a node to be children of another node
you can do this:
use App\Models\Node; $node = Node::find(10); $another_node = Node::find(30); $node->moveChildrenTo($another_node);
Note: When trying to move node1 to node2, if node2 is descendant of node1
TreeException
will be thrown.
The same exception will be thrown if node2 is a female node.
To move a node after another node you can do this:
use App\Models\Node; $node = Node::find(10); $another_node = Node::find(30); $node->moveAfter($another_node);
To move a node before another node you can do this:
use App\Models\Node; $node = Node::find(10); $another_node = Node::find(30); $node->moveBefore($another_node);
Note: When trying to move the Root or when trying to move a node after or before one of its descendants
TreeException
will be thrown.
to get the generation number of a node in a tree:
return $node->generation(); // int or null
Note: You can use the Pointer of tree to access all methods of class Node.
for example:
use Girover\Tree\Models\Tree; $tree = Tree::find(1); $tree->pointer()->to('aa.aa')->father(); // move Pointer to location 'aa.aa' and then get its father. $tree->pointer()->grandfather(); // get grandfather of node that Pointer indicates to $tree->pointer()->ancestor(3); // get father of node that Pointer indicates to $tree->pointer()->children(); // get children of node that Pointer indicates to $tree->pointer()->sons(); // get sons of node that Pointer indicates to $tree->pointer()->newDaughter($data); // create daughter for the node that Pointer indicates to $tree->pointer()->newSon($data); // create son of node that Pointer indicates to $tree->pointer()->newSister($data); // create sister for the node that Pointer indicates to $tree->pointer()->newChild($data, 'm'); // create son for the node that Pointer indicates to $tree->pointer()->firstChild(); // get the first child of node that Pointer indicates to . . . . . $tree->pointer()->toHtml();
Attaching and Detaching
When creating nodeable models by using methods create
or save
, the created model will not be attached with any node in any tree.
use App\Models\Person; $person = Person::create(['name'=>'Person', 'birth_date'=>'2000-01-01']); $another_person = new Person; $another_person->name = 'Another'; $another_person->birth_date = '2000-01-01'; $another_person->save();
Both $person and $another_person will be created in database table, but they will not be a node yet in any tree.
To make Adam as a node in a tree you can use all nodeable methods like:
use App\Models\Person; $new_person = Person::where('name', 'new person')->first(); $person = Person::find(1); $person->newSon($new_person); //or $person->createFather($new_person); // only if $person is a root in the tree //or $person->newBrother($new_person); // only if $person is not a root in the tree //or . . .
detaching
When deleting nodeable models they will be deleted from database table, but what if you need to delete(detach) them from a tree while keeping them in database.
Let's look at the following code:
use App\Models\Person; $person = Person::find(100); $person->delete();
In the code above, the person will be deleted from database with all its descendants.
But we need only to delete it from a tree not from database. Look at this code:
use App\Models\Person; $person = Person::find(100); $person->detachFromTree();
Now the person still exists in database, but it is not a node in any tree anymore.
Helpers
To get all detached nodeable models, you can use the helper function detachedNodeables
.
This will return instance of \Illuminate\Database\Eloquent\collection
with all detached nodeable models.
<?php $detached = detachedNodeables();
To get the count of detached nodeable models, use the helper function countDetachedNodeables
.
<?php $count_detached = countDetachedNodeables();
Rendering trees
To show a tree in the browser you can use one of these methods toHtml
, toTree
or draw
on an object of the Treeable model eg. App\Models\Tree
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Tree; class TreeController extends Controller { public function index() { $tree = Tree::find(1); $treeHTML = $tree->toHtml() // OR $treeHTML = $tree->toTree() // OR $treeHTML = $tree->Draw() return view('tree.index')->with('treeHTML', $treeHTML); } }
And now inside your blade file you can render the tree by using custom blade directive
@tree($treeHtml)
or by using this syntax {!! toHtml !!}
.
<!-- views/tree/index.blade.php --> <div> @tree($treeHtml) </div> <!-- OR --> <div> {!! $treeHTML !!} </div>
Note the above example will render the entire tree, but if you want to render a subtree starting from a specific node you can use the Pinter to do that:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Tree; use App\Models\Node; class TreeController extends Controller { public function index() { $tree = Tree::find(1); $person = Node::find(11); $treeHTML = $tree->pointer()->to($person)->toHtml(); // OR $treeHTML = $tree->pointer()->to($person)->toTree(); // OR $treeHTML = $tree->pointer()->to($person)->draw(); return view('tree.index')->with('treeHTML', $treeHTML); } }
You can also use a node model to do the same by using one of these methods toHtml
, toTree
or draw
on an object of the nodeable model eg. App\Models\Node
:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\Node; class TreeController extends Controller { public function index() { $person = Node::find(11); $treeHTML = $person->toHtml(); // OR $treeHTML = $person->toTree(); // OR $treeHTML = $person->draw(); return view('tree.index')->with('treeHTML', $treeHTML); } }
Customizing node style
If you want to add some html attributes to the nodes, then you can bind a custom function called nodeHtmlAttributes
to the Service Container. This function must return a callable function which accept one argument represents nodeable model. And this callable function must return html attributes with their values as one string.
Let's take an example.
If your nodeable models have fields id, name, gender, is_died
, then you want to add them as html attributes to the nodes to style or to control nodes.
// AppServiceProvider public function boot() { $this->app->singleton('nodeHtmlAttributes',function($app){ return function($nodeable){ return " data-id='{$nodeable->id}' data-name='{$nodeable->name}' data-gender='{$nodeable->gender}' data-is-died='{$nodeable->is_died}' "; }; }); }
Now all node elements will have these attributes like:
<a class="node" data-id='1' data-name='name' data-gender='1' data-is-died='0'>...</a>
If you want to add some css classes to the nodes that have specific role, then you can bind a custom function called nodeCssClass
to the Service Container. This function must return a callable function which accept one argument represents nodeable model. And this callable function must return css classes names as string.
Let's take an example.
If your nodeable models have an attribute called is_died
which has values true
or false
, then you want to add a css class called is-died
to the nodes that have this value as true
to give them more style.
// AppServiceProvider public function boot() { $this->app->singleton('nodeCssClasses',function($app){ return function($nodeable){ return ($nodeable->is_died) ? 'is-died' :''; }; }); }
Now all nodeables that have is_died
to true will be like:
<a class="node is-died">...</a>
Testing
./vendor/bin/PHPUnit
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.