README.md 5.73 KB
Newer Older
Lukáš Zahradník's avatar
Lukáš Zahradník committed
# MPTT

Lukáš Zahradník's avatar
Lukáš Zahradník committed
## Installation

In `composer.json` insert into `repositories` section
```
{
	"type": "vcs",
Lukáš Zahradník's avatar
Lukáš Zahradník committed
	"url": "ssh://git@gitlab.mondayfactory.cz:2222/mondayfactory/mptt.git"
Lukáš Zahradník's avatar
Lukáš Zahradník committed
}
```

and require package using `composer require monday-factory/mptt`

----

## CoreMPTT

`CoreMPTT` implements the logic of MPTT. Extend that class in your custom MPTT class implementation.

## Node

Lukáš Zahradník's avatar
Lukáš Zahradník committed
`Node` coresponds to row in database table. It contains some data as `id`, `parent`, `left`, `right` etc. that are important for MPTT functionality, but can contain some custom data as well.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

## NodeCollection

Lukáš Zahradník's avatar
Lukáš Zahradník committed
`NodeCollection` is collection of objects of `Node` type. It works as dictionary/hash map/associative array, where key is node id and value is node.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

----

## Example Usage

### Database table

Let's have table called `mptt_warehouses` in our database with following scheme

| id   | name | parent_id | left | right | some other columns... | 
| ---- | ---- | --------- | ---- | ----- | --------------------- |
| Uuid | root | null      |  1   |  4    | my custom data        |
| Uuid | cat1 | null      |  2   |  3    | my custom data        |


Lukáš Zahradník's avatar
Lukáš Zahradník committed
Every database table, that we want to use for mptt, should have columns `id (char(36))`, `name (varchar)`, `parent_id (char(36))`, `left (int(11))`, `right (int(11))`. Names of these column can be modified by setting/overriding CoreMPTT's properties. 
Lukáš Zahradník's avatar
Lukáš Zahradník committed


### MPTT Class

Now we create class extending `MondayFactory\MPTT\CoreMPTT` class for our previously defined table

```php
<?php

declare(strict_types=1);

namespace App\MPTT;

use Dibi\Connection;
use Dibi\Fluent;
use MondayFactory\MPTT\CoreMPTT;
use MondayFactory\MPTT\Node\Node;

final class WarehousesMPTT extends CoreMPTT
{

	/**
	 * @var string
	 */
	protected $tablename = 'mptt_warehouses';           // We override CoreMPPT property $tablename with name of our table


	public function __construct(Connection $connection) 
	{
		parent::__construct($connection);
	}


	protected function restrict(Fluent $query, string $alias): Fluent
	{
		return $query;
	}


	protected function appendResource(Fluent $query): Fluent
	{
		return $query;
	}


	protected function getDataFromRow(array $row): ?array
	{
		return null;
	}


	protected function getCustomFields(Node $node): array
	{
		return [];
	}


	protected function appendSelect(): string
	{
		return '';
	}
}

```

Now we have minimal MPTT class implementation and we are good to go.

### Advanced usage

#### Custom fields

We can have some custom columns in our table.
Lukáš Zahradník's avatar
Lukáš Zahradník committed
There is function `getCustomFields(Node $node): array` to specify column names and values for inserting.
Lukáš Zahradník's avatar
Lukáš Zahradník committed
Let's add new column `company_id` into our table where we will store for which company the node is assigned to.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

We return key, value array of our custom columns and these will be inserted into table with our node. 
```php
protected function getCustomFields(Node $node): array
{
	return [
		'company_id' => SOME_ID,
	];
}
```

Lukáš Zahradník's avatar
Lukáš Zahradník committed
To get and work with these custom fields, see **Append Resource** section
Lukáš Zahradník's avatar
Lukáš Zahradník committed

#### Restrict

Previously shown example is only usable, when there is one tree in one table. If we want to have multiple trees in table, we have to somehow restrict selection of tree.
In that case there is function `restrict(Fluent $query, string $alias): Fluent`.

Lukáš Zahradník's avatar
Lukáš Zahradník committed
We modify our impementation of `restrict` function.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

```php
protected function restrict(Fluent $query, string $alias): Fluent
{
    return $query->where(
        "%n.company_id = ?", $alias, SOME_ID
    );
}
```

Lukáš Zahradník's avatar
Lukáš Zahradník committed
We add constraint on our query to select rows only where `company_id` equals to `SOME_ID` (`SOME_ID` is placeholder, can be passed in constructor or anywhere else).
`alias` is used to distinguish between `node` and `parent` tables, but resticting should be almost always same for both of them.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

#### Append Resource

Sometimes we have some data in other tables, related to nodes, that we would like to get with our tree nodes.
Lukáš Zahradník's avatar
Lukáš Zahradník committed
Let's have another table called `warehouses` with some columns and column `mptt_id`. `mptt_id` is reference to `id` in `mptt_warehouses`.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

We would like to select row from `mptt_warehouses` with appended rows from `warehouses`. We use function `appendResource(Fluent $query): Fluent`.
```php
protected function appendResource(Fluent $query): Fluent
{
	return $query
		->leftJoin('warehouses')
		->on('`warehouses`.`mptt_id` = %n.%n', $this->nodeAlias, $this->primaryKey);
}
```

We basically just join table with `warehouses`.
Lukáš Zahradník's avatar
Lukáš Zahradník committed
Now we modify query selection to select some column from `warehouses` table, so we can work with these columns during tree build.
Lukáš Zahradník's avatar
Lukáš Zahradník committed

```php
protected function appendSelect(): string
{
	return '`warehouses`.`id` as warehouses_id, `warehouses`.`description` as warehouses_name';
}
```

Next, we implement function `getDataFromRow(array $row): ?array`, so we have array of our custom data from `warehouses` in Node `data` array.
We can use this method to insert custom fields into Node's data.

```php
protected function getDataFromRow(array $row): ?array
{
	if ($row['warehouses_id'] === null || $row['warehouses_name'] === null) {
		return null;
	}

	return [
		'id' => $row['warehouses_id'],
		'name' => $row['warehouses_name'],
	];
}
```
Array returned by this function is appended into node's data property.

#### Clean up

Sometimes we need to do something when we delete nodes, for example set `mptt_id` in related rows in `warehouses` to parent of deleted node.
We can simply do that via function `onDeleteNode(string $key, string $parent): void`.

Example usage:
```php
public function onDelete(string $key, string $parent): void
{
	$this->connection
		->update('warehouses', ['mptt_id' => $parent])
		->where('`mptt_id` = ?', $key)
		->execute();
}
```

Petr Besir Horáček's avatar
Petr Besir Horáček committed
and register callback in constructor (`onNodeInsert` and `onNodeUpdate` are also avaible)
Lukáš Zahradník's avatar
Lukáš Zahradník committed
```php
Lukáš Zahradník's avatar
Lukáš Zahradník committed
$this->onNodeDelete = [$this, 'onDelete'];