Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sam152 committed Nov 6, 2023
0 parents commit ee08c45
Show file tree
Hide file tree
Showing 13 changed files with 557 additions and 0 deletions.
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
Treenum: tree + enum in PHP
===

Treenum is a lightweight library for defining and traversing items in an enum, that have a tree structure.

```
composer require sam152/treenum
```

## Defining a tree

Trees may be defined either by identifying the parents or the children of any given item, whichever is easier for the consumer.

Example of a tree identified by declaring children of any given item:

```php
enum Pet implements TreeEnum {
use GetChildrenImplementation;

case Dog;
case Retriever;
case Labrador;
case Golden;
case Terrier;
case Bird;
case Chicken;
case Cat;

public function getChildren(): array {
return match ($this) {
static::Dog => [
static::Retriever,
static::Terrier,
],
static::Retriever => [
static::Labrador,
static::Golden,
],
static::Bird => [
static::Chicken,
],
default => [],
};
}
}
```

And the same tree identified by declaring the parent of any given item:

```php
enum Pet implements TreeEnum {
use GetParentImplementation;

case Dog;
case Retriever;
case Labrador;
case Golden;
case Terrier;
case Bird;
case Chicken;
case Cat;

public function getParent(): static|null {
return match($this) {
static::Labrador, static::Golden => static::Retriever,
static::Retriever, static::Terrier => static::Dog,
static::Chicken => static::Bird,
default => null,
};
}
}

```

## Public API

The following methods are defined on `TreeEnum` and can be used to traverse the tree:

```php
public function getAncestors(): array;
public function getDescendants(): array;
public function getChildren(): array;
public function getParent(): static | null;
public function getDepth(): int;
public static function rootCases(): array;
public static function leafCases(): array;
```

### Example usage:

```php
php > var_export(Pet::Dog->getChildren());
array (
Pet::Retriever,
Pet::Terrier,
)
```

```php
php > var_export(Pet::rootCases());
array (
Pet::Dog,
Pet::Bird,
Pet::Cat,
)
```

### Additional helpers

```php
php > print \Treenum\Internal\Utility::dumpTree(Pet::class);
.
├── Dog
│ ├── Retriever
│ │ ├── Labrador
│ │ └── Golden
│ └── Terrier
├── Bird
│ └── Chicken
└── Cat
```
22 changes: 22 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "sam152/treenum",
"description": "PHP trees based on enums.",
"type": "library",
"license": "MIT",
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10"
},
"autoload": {
"psr-4": {
"Treenum\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Treenum": "tests/src/"
}
}
}
16 changes: 16 additions & 0 deletions src/GetChildrenImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Treenum;

use Treenum\Internal\BaseImplementation;
use Treenum\Internal\Utility;

trait GetChildrenImplementation {
use BaseImplementation;

public function getParent(): static | null {
return Utility::find(static::cases(), fn (TreeEnum $item) => \in_array($this, $item->getChildren(), true));
}
}
15 changes: 15 additions & 0 deletions src/GetParentImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Treenum;

use Treenum\Internal\BaseImplementation;

trait GetParentImplementation {
use BaseImplementation;

public function getChildren(): array {
return array_values(array_filter(static::cases(), fn (TreeEnum $item) => $item->getParent() === $this));
}
}
38 changes: 38 additions & 0 deletions src/Internal/BaseImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Treenum\Internal;

use Treenum\TreeEnum;

/**
* @internal
*/
trait BaseImplementation {
public function getDepth(): int {
return Utility::recurse(
fn (callable $fn, TreeEnum $item, $depth = 1) => $item->getParent() ? $fn($fn, $item->getParent(), $depth + 1) : $depth,
$this
);
}

public static function rootCases(): array {
return array_values(array_filter(static::cases(), fn (TreeEnum $item) => null === $item->getParent()));
}

public static function leafCases(): array {
return array_values(array_filter(static::cases(), fn (TreeEnum $item) => empty($item->getChildren())));
}

public function getAncestors(): array {
return Utility::recurse(
fn (callable $fn, TreeEnum $item, $parents = []) => $item->getParent() ? $fn($fn, $item->getParent(), array_merge($parents, [$item->getParent()])) : $parents,
$this
);
}

public function getDescendants(): array {
return array_merge($this->getChildren(), ...array_map(fn (TreeEnum $item) => $item->getDescendants(), $this->getChildren()));
}
}
51 changes: 51 additions & 0 deletions src/Internal/Utility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace Treenum\Internal;

use Treenum\TreeEnum;

final class Utility {
/**
* @template T
*
* @param array<T> $array
*
* @return T|null
*/
public static function find(array $array, callable $search): mixed {
foreach ($array as $key => $value) {
if ($search($value, $key)) {
return $value;
}
}
return null;
}

/**
* Allow arrow functions to recurse in a single expression, by passing the fn as the first argument.
*/
public static function recurse(callable $arrowFn, ...$args) {
return $arrowFn($arrowFn, ...$args);
}

/**
* @param class-string<\Treenum\TreeEnum> $enum
*/
public static function dumpTree(string $enum): string {
return self::recurse(function (callable $printTree, array $nodes, $prefix = '') {
return (empty($prefix) ? ".\n" : '') . array_reduce($nodes, function (string $tree, TreeEnum $node) use ($nodes, $prefix, $printTree) {
$isLast = ($node === end($nodes));
$tree .= sprintf(
"%s%s %s\n",
$prefix,
$isLast ? '└──' : '├──',
$node->name,
);
$tree .= $printTree($printTree, $node->getChildren(), $isLast ? $prefix . ' ' : $prefix . '');
return $tree;
}, '');
}, $enum::rootCases());
}
}
40 changes: 40 additions & 0 deletions src/TreeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Treenum;

interface TreeEnum extends \UnitEnum {
/**
* @return static[]
*/
public function getAncestors(): array;

/**
* @return static[]
*/
public function getDescendants(): array;

/**
* @return static[]
*/
public function getChildren(): array;

public function getParent(): static | null;

public function getDepth(): int;

/**
* The root cases are those at a depth of 1, which may or may not have children.
*
* @return static[]
*/
public static function rootCases(): array;

/**
* Leaf cases are the deepest items in each branch of the tree, those without any children.
*
* @return static[]
*/
public static function leafCases(): array;
}
38 changes: 38 additions & 0 deletions tests/src/Fixtures/Pet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Tests\Treenum\Fixtures;

use Treenum\GetChildrenImplementation;
use Treenum\TreeEnum;

enum Pet implements TreeEnum {
use GetChildrenImplementation;

case Dog;
case Retriever;
case Labrador;
case Golden;
case Terrier;
case Bird;
case Chicken;
case Cat;

public function getChildren(): array {
return match ($this) {
static::Dog => [
static::Retriever,
static::Terrier,
],
static::Retriever => [
static::Labrador,
static::Golden,
],
static::Bird => [
static::Chicken,
],
default => [],
};
}
}
30 changes: 30 additions & 0 deletions tests/src/Fixtures/PetWithParent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Tests\Treenum\Fixtures;

use Treenum\GetParentImplementation;
use Treenum\TreeEnum;

enum PetWithParent implements TreeEnum {
use GetParentImplementation;

case Dog;
case Retriever;
case Labrador;
case Golden;
case Terrier;
case Bird;
case Chicken;
case Cat;

public function getParent(): static|null {
return match($this) {
static::Labrador, static::Golden => static::Retriever,
static::Retriever, static::Terrier => static::Dog,
static::Chicken => static::Bird,
default => null,
};
}
}
16 changes: 16 additions & 0 deletions tests/src/GetChildrenImplementationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Tests\Treenum;

use PHPUnit\Framework\TestCase;
use Tests\Treenum\Fixtures\Pet;

class GetChildrenImplementationTest extends TestCase {
use TestCases;

protected static function testWith(): string {
return Pet::class;
}
}
Loading

0 comments on commit ee08c45

Please sign in to comment.