Browse Source

Add tree parent info and add TypeMap behavior.

mscherer 7 years ago
parent
commit
2fccd243ab

+ 2 - 1
.travis.yml

@@ -47,8 +47,9 @@ before_script:
 
 script:
   - if [[ $DEFAULT == 1 ]]; then vendor/bin/phpunit; fi
-  - if [[ $CHECKS == 1 ]]; then vendor/bin/phpcs -p --extensions=php --standard=vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml --ignore=/tests/test_files/ src tests config ; fi
+
   - if [[ $CHECKS == 1 ]]; then composer phpstan-setup && composer phpstan ; fi
+  - if [[ $CHECKS == 1 ]]; then composer cs-check ; fi
 
   - if [[ $CODECOVERAGE == 1 ]]; then vendor/bin/phpunit --coverage-clover=clover.xml || true; fi
   - if [[ $CODECOVERAGE == 1 ]]; then wget -O codecov.sh https://codecov.io/bash; fi

+ 2 - 2
composer.json

@@ -50,8 +50,8 @@
 		"test": "php phpunit.phar",
 		"test-setup": "[ ! -f phpunit.phar ] && wget https://phar.phpunit.de/phpunit-5.7.20.phar && mv phpunit-5.7.20.phar phpunit.phar || true",
 		"test-coverage": "php phpunit.phar --log-junit webroot/coverage/unitreport.xml --coverage-html webroot/coverage --coverage-clover webroot/coverage/coverage.xml",
-		"cs-check": "phpcs -p --standard=vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml --ignore=/cakephp-tools/vendor/,/tmp/,/logs/,/tests/test_files/ --extensions=php ./",
-		"cs-fix": "phpcbf -v --standard=vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml --ignore=/cakephp-tools/vendor/,/tmp/,/logs/,/tests/test_files --extensions=php ./"
+		"cs-check": "phpcs -p --standard=vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml --extensions=php --ignore=/tests/test_files/ src/ tests/ config/",
+		"cs-fix": "phpcbf -v --standard=vendor/fig-r/psr2r-sniffer/PSR2R/ruleset.xml --extensions=php --ignore=/tests/test_files/ src/ tests/ config/"
 	},
 	"config": {
 		"process-timeout": 600

+ 69 - 0
docs/Helper/Tree.md

@@ -0,0 +1,69 @@
+# Tree Helper
+
+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.
+
+### Usage
+
+#### Basic usage
+Include helper in your AppView class as
+```php
+$this->addHelper('Tools.Tree', [
+	...
+]);
+```
+
+Then you can use it in your templates as
+```php
+echo $this->Tree->generate($articles);
+```
+
+#### Templated usage
+By default, just outputting the display name is usually not enough.
+You want to create some `Template/Element/tree_element.ctp` element instead:
+
+```php
+echo $this->Tree->generate($articles, ['element' => 'tree_element']);
+```
+
+That template can then contain all normal template additions, including full helper access:
+
+```php
+<?php
+/**
+ * @var \App\View\AppView $this
+ * @var \App\Model\Entity\Article|\Cake\Collection\CollectionInterface $data
+ */
+
+if (!$data->visible) { // You can do anything here depending on the record content
+	return;
+}
+?>
+<li>
+<?php echo $this->Html->link($data->title, ['action' => 'view', $data->id]); ?>
+</li>
+```
+
+So the current entity object is available as `$data` variable inside this snippet.
+
+### Available element/callback data
+
+- $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. 
+
+### Outview
+
+You can read some more tutorial like details in [my blog post](http://www.dereuromark.de/2013/02/17/cakephp-and-tree-structures/).

+ 1 - 0
docs/README.md

@@ -47,6 +47,7 @@ Helpers:
 * [Form](Helper/Form.md)
 * [Common](Helper/Common.md)
 * [Format](Helper/Format.md)
+* [Tree](Helper/Tree.md)
 * [Typography](Helper/Typography.md)
 
 Widgets:

+ 50 - 0
src/Model/Behavior/TypeMapBehavior.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Tools\Model\Behavior;
+
+use Cake\ORM\Behavior;
+use RuntimeException;
+
+/**
+ * A behavior that will allow changing a table's field types on the fly.
+ *
+ * Usage: See docs
+ *
+ * @author Mark Scherer
+ * @license MIT
+ */
+class TypeMapBehavior extends Behavior {
+
+	/**
+	 * @var array
+	 */
+	protected $_defaultConfig = [
+		'fields' => [], // Fields to change column type for
+	];
+
+	/**
+	 * @param array $config
+	 * @throws \RuntimeException
+	 * @return void
+	 */
+	public function initialize(array $config = []) {
+		if (empty($this->_config['fields'])) {
+			throw new RuntimeException('Fields are required');
+		}
+		if (!is_array($this->_config['fields'])) {
+			$this->_config['fields'] = (array)$this->_config['fields'];
+		}
+
+		foreach ($this->_config['fields'] as $field => $type) {
+			if (is_array($type)) {
+				$type = $field['type'];
+			}
+			if (!is_string($type)) {
+				throw new RuntimeException('Invalid field type setup.');
+			}
+
+			$this->_table->getSchema()->setColumnType($field, $type);
+		}
+	}
+
+}

+ 52 - 40
src/View/Helper/TreeHelper.php

@@ -17,7 +17,6 @@ use Exception;
 /**
  * Helper to generate tree representations of MPTT or recursively nested data.
  *
- * @deprecated Use https://github.com/ADmad/cakephp-tree instead.
  * @author Andy Dawson
  * @author Mark Scherer
  * @link http://www.dereuromark.de/2013/02/17/cakephp-and-tree-structures/
@@ -47,6 +46,7 @@ class TreeHelper extends Helper {
 		'maxDepth' => 999,
 		'firstChild' => true,
 		'indent' => null,
+		'indentWith' => "\t",
 		'splitDepth' => false,
 		'splitCount' => null,
 		'totalNodes' => null,
@@ -113,16 +113,29 @@ class TreeHelper extends Helper {
 	 *    'splitCount' => the number of "parallel" types. defaults to null (disabled) set the splitCount,
 	 *        and optionally set the splitDepth to get parallel lists
 	 *
-	 * @param array|\Cake\Orm\Query $data Data to loop over
+	 * @param array|\Cake\Datasource\QueryInterface|\Cake\ORM\ResultSet $data Data to loop over
 	 * @param array $config Config
 	 * @return string HTML representation of the passed data
 	 * @throws \Exception
 	 */
 	public function generate($data, array $config = []) {
-		if (is_object($data)) {
-			$data = $data->toArray();
+		return $this->_generate($data, $config);
+	}
+
+	/**
+	 * @param array|\Cake\Datasource\QueryInterface|\Cake\ORM\ResultSet $data
+	 * @param array $config
+	 * @param array|\Cake\Datasource\QueryInterface|\Cake\ORM\ResultSet|null $parent
+	 *
+	 * @throws \Exception
+	 * @return string
+	 */
+	protected function _generate($data, array $config, $parent = null) {
+		$dataArray = $data;
+		if (is_object($dataArray)) {
+			$dataArray = $data->toArray();
 		}
-		if (!$data) {
+		if (!$dataArray) {
 			return '';
 		}
 
@@ -147,8 +160,8 @@ class TreeHelper extends Helper {
 		}
 		$return = '';
 		$addType = true;
-		$this->_config['totalNodes'] = count($data);
-		$keys = array_keys($data);
+		$this->_config['totalNodes'] = count($dataArray);
+		$keys = array_keys($dataArray);
 
 		if ($hideUnrelated === true || is_numeric($hideUnrelated)) {
 			$this->_markUnrelatedAsHidden($data, $treePath);
@@ -156,25 +169,17 @@ class TreeHelper extends Helper {
 			call_user_func($hideUnrelated, $data, $treePath);
 		}
 
-		foreach ($data as $i => &$result) {
-			/* Allow 2d data arrays */
-			if (is_object($result)) {
-				$result = $result->toArray();
-			}
-			if ($model && isset($result->$model)) {
-				$row = &$result->$model;
-			} else {
-				$row = &$result;
-			}
+		foreach ($data as $i => $result) {
+			$row = $result;
 
 			/* Close open items as appropriate */
 			// @codingStandardsIgnoreStart
-			while ($stack && ($stack[count($stack)-1] < $row[$right])) {
+			while ($stack && ($stack[count($stack) - 1] < $row[$right])) {
 				// @codingStandardsIgnoreEnd
 				array_pop($stack);
 				if ($indent) {
-					$whiteSpace = str_repeat("\t", count($stack));
-					$return .= "\r\n" . $whiteSpace . "\t";
+					$whiteSpace = str_repeat($indentWith, count($stack));
+					$return .= "\r\n" . $whiteSpace . $indentWith;
 				}
 				if ($type) {
 					$return .= '</' . $type . '>';
@@ -234,6 +239,7 @@ class TreeHelper extends Helper {
 
 			$elementData = [
 				'data' => $result,
+				'parent' => $parent,
 				'depth' => $depth,
 				'hasChildren' => $hasChildren,
 				'numberOfDirectChildren' => $numberOfDirectChildren,
@@ -266,9 +272,9 @@ class TreeHelper extends Helper {
 			if (!$content) {
 				continue;
 			}
-			$whiteSpace = str_repeat("\t", $depth);
+			$whiteSpace = str_repeat($indentWith, $depth);
 			if ($indent && strpos($content, "\r\n", 1)) {
-				$content = str_replace("\r\n", "\n" . $whiteSpace . "\t", $content);
+				$content = str_replace("\r\n", "\n" . $whiteSpace . $indentWith, $content);
 			}
 			/* Prefix */
 			if ($addType) {
@@ -281,7 +287,7 @@ class TreeHelper extends Helper {
 				}
 			}
 			if ($indent) {
-				$return .= "\r\n" . $whiteSpace . "\t";
+				$return .= "\r\n" . $whiteSpace . $indentWith;
 			}
 			if ($itemType) {
 				$itemAttributes = $this->_attributes($itemType, $elementData);
@@ -293,11 +299,14 @@ class TreeHelper extends Helper {
 			if ($hasVisibleChildren) {
 				if ($numberOfDirectChildren) {
 					$config['depth'] = $depth + 1;
+					$children = $result['children'];
+					//unset($result['children']);
+
 					$return .= $this->_suffix();
-					$return .= $this->generate($result['children'], $config);
+					$return .= $this->_generate($children, $config, $result);
 					if ($itemType) {
 						if ($indent) {
-							$return .= $whiteSpace . "\t";
+							$return .= $whiteSpace . $indentWith;
 						}
 						$return .= '</' . $itemType . '>';
 					}
@@ -316,8 +325,8 @@ class TreeHelper extends Helper {
 		while ($stack) {
 			array_pop($stack);
 			if ($indent) {
-				$whiteSpace = str_repeat("\t", count($stack));
-				$return .= "\r\n" . $whiteSpace . "\t";
+				$whiteSpace = str_repeat($indentWith, count($stack));
+				$return .= "\r\n" . $whiteSpace . $indentWith;
 			}
 			if ($type) {
 				$return .= '</' . $type . '>';
@@ -339,6 +348,7 @@ class TreeHelper extends Helper {
 				$return .= "\r\n";
 			}
 		}
+
 		return $return;
 	}
 
@@ -348,9 +358,9 @@ class TreeHelper extends Helper {
 	 * Called to modify the attributes of the next <item> to be processed
 	 * Note that the content of a 'node' is processed before generating its wrapping <item> tag
 	 *
-	 * @param string $id
-	 * @param string $key
-	 * @param mixed $value
+	 * @param string $id Id
+	 * @param string $key Key
+	 * @param mixed $value Value
 	 * @return void
 	 */
 	public function addItemAttribute($id = '', $key = '', $value = null) {
@@ -383,10 +393,10 @@ class TreeHelper extends Helper {
 	 * // give top level type (1) a class
 	 * $tree->addTypeAttribute('class', 'hasHiddenGrandChildren', null, 'previous');
 	 *
-	 * @param string $id
-	 * @param string $key
-	 * @param mixed|null $value
-	 * @param string $previousOrNext
+	 * @param string $id ID
+	 * @param string $key Key
+	 * @param mixed|null $value Value
+	 * @param string $previousOrNext Previous or next
 	 * @return void
 	 */
 	public function addTypeAttribute($id = '', $key = '', $value = null, $previousOrNext = 'next') {
@@ -407,7 +417,7 @@ class TreeHelper extends Helper {
 	 *
 	 * Used to close and reopen a ul/ol to allow easier listings
 	 *
-	 * @param bool $reset
+	 * @param bool $reset Reset
 	 * @return string
 	 */
 	protected function _suffix($reset = false) {
@@ -441,6 +451,7 @@ class TreeHelper extends Helper {
 				$_splitCounter++;
 				if ($type && ($_splitCounter % $_splitCount) === 0 && !$lastChild) {
 					unset($this->_config['callback']);
+
 					return '</' . $type . '><' . $type . '>';
 				}
 			}
@@ -452,9 +463,9 @@ class TreeHelper extends Helper {
 	 *
 	 * Logic to apply styles to tags.
 	 *
-	 * @param string $rType
-	 * @param array $elementData
-	 * @param bool $clear
+	 * @param string $rType rType
+	 * @param array $elementData Element data
+	 * @param bool $clear Clear
 	 * @return string
 	 */
 	protected function _attributes($rType, array $elementData = [], $clear = true) {
@@ -497,6 +508,7 @@ class TreeHelper extends Helper {
 			}
 			$attributes[$type] = $type . '="' . implode(' ', $attributes[$type]) . '"';
 		}
+
 		return ' ' . implode(' ', $attributes);
 	}
 
@@ -506,7 +518,7 @@ class TreeHelper extends Helper {
 	 *
 	 * @param array $tree Tree
 	 * @param array $path Tree path
-	 * @param int $level
+	 * @param int $level Level
 	 * @return void
 	 * @throws \Exception
 	 */
@@ -521,7 +533,7 @@ class TreeHelper extends Helper {
 				throw new Exception('Only works with threaded (nested children) results');
 			}
 
-			if (!empty($path[$level]) && $subTree['id'] == $path[$level]['id']) {
+			if (!empty($path[$level]) && $subTree['id'] == $path[$level]) {
 				$subTree['show'] = 1;
 				$siblingIsActive = true;
 			}

+ 37 - 0
tests/Fixture/DataFixture.php

@@ -0,0 +1,37 @@
+<?php
+namespace Tools\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+class DataFixture extends TestFixture {
+
+	/**
+	 * Fields
+	 *
+	 * @var array
+	 */
+	public $fields = [
+		'id' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
+		'name' => ['type' => 'string', 'length' => 50, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null],
+		'data_json' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
+		'data_array' => ['type' => 'text', 'length' => 16777215, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],
+		'_constraints' => [
+			'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
+		],
+	];
+
+	/**
+	 * Records
+	 *
+	 * @var array
+	 */
+	public $records = [
+			[
+				'id' => 1,
+				'name' => 'Lorem ipsum dolor sit amet',
+				'data_json' => null,
+				'data_array' => null,
+			],
+		];
+
+}

+ 80 - 0
tests/TestCase/Model/Behavior/TypeMapBehaviorTest.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace Tools\Test\TestCase\Model\Behavior;
+
+use Cake\ORM\TableRegistry;
+use Tools\TestSuite\TestCase;
+
+class TypeMapBehaviorTest extends TestCase {
+
+	/**
+	 * @var \Tools\Model\Behavior\TypeMapBehavior
+	 */
+	public $TypeMapBehavior;
+
+	/**
+	 * @var \Tools\Model\Table\Table
+	 */
+	protected $Table;
+
+	/**
+	 * @var array
+	 */
+	public $fixtures = ['plugin.Tools.Data'];
+
+	/**
+	 * @return void
+	 */
+	public function setUp() {
+		parent::setUp();
+	}
+
+	/**
+	 * Tests that we can disable array conversion for edit forms if we need to modify the JSON directly.
+	 *
+	 * @return void
+	 */
+	public function testFields() {
+		$this->Table = TableRegistry::get('Data');
+		$this->Table->addBehavior('Tools.Jsonable', ['fields' => ['data_array']]);
+
+		$entity = $this->Table->newEntity();
+
+		$data = [
+			'name' => 'FooBar',
+			'data_json' => ['x' => 'y'],
+			'data_array' => ['x' => 'y'],
+		];
+		$entity = $this->Table->patchEntity($entity, $data);
+		$this->assertEmpty($entity->getErrors());
+
+		$this->Table->saveOrFail($entity);
+		$this->assertSame($data['data_json'], $entity->data_json);
+		$this->assertSame('{"x":"y"}', $entity->data_array);
+
+		$savedEntity = $this->Table->get($entity->id);
+
+		$this->assertSame($data['data_json'], $savedEntity->data_json);
+		$this->assertSame($data['data_array'], $savedEntity->data_array);
+
+		// Now let's disable the array conversion per type
+		$this->Table->removeBehavior('Jsonable');
+		$this->Table->addBehavior('Tools.TypeMap', ['fields' => ['data_json' => 'text']]);
+		$entity = $this->Table->get($entity->id);
+
+		$this->assertSame('{"x":"y"}', $entity->data_json);
+		$this->assertSame('{"x":"y"}', $entity->data_array);
+
+		$data = [
+			'data_json' => '{"x":"z"}',
+			'data_array' => '{"x":"z"}',
+		];
+		$entity = $this->Table->patchEntity($entity, $data);
+		$this->Table->saveOrFail($entity);
+
+		$savedEntity = $this->Table->get($entity->id);
+		$this->assertSame($data['data_json'], $savedEntity->data_json);
+		$this->assertSame($data['data_array'], $savedEntity->data_array);
+	}
+
+}

+ 91 - 40
tests/TestCase/View/Helper/TreeHelperTest.php

@@ -4,7 +4,6 @@ namespace Tools\Test\TestCase\View\Helper;
 
 use Cake\Datasource\ConnectionManager;
 use Cake\ORM\Entity;
-use Cake\ORM\Table;
 use Cake\ORM\TableRegistry;
 use Cake\View\View;
 use Tools\TestSuite\TestCase;
@@ -22,7 +21,12 @@ class TreeHelperTest extends TestCase {
 	/**
 	 * @var \Cake\ORM\Table
 	 */
-	public $Table;
+	protected $Table;
+
+	/**
+	 * @var \Tools\View\Helper\TreeHelper
+	 */
+	protected $Tree;
 
 	/**
 	 * Initial Tree
@@ -46,13 +50,11 @@ class TreeHelperTest extends TestCase {
 		$this->Table = TableRegistry::get('AfterTrees');
 		$this->Table->addBehavior('Tree');
 
-		//$this->Table->truncate();
 		$connection = ConnectionManager::get('test');
 		$sql = $this->Table->getSchema()->truncateSql($connection);
 		foreach ($sql as $snippet) {
 			$connection->execute($snippet);
 		}
-		//$this->Table->deleteAll(array());
 
 		$data = [
 			['name' => 'One'],
@@ -87,15 +89,6 @@ class TreeHelperTest extends TestCase {
 	/**
 	 * @return void
 	 */
-	public function testObject() {
-		$this->assertInstanceOf('Tools\View\Helper\TreeHelper', $this->Tree);
-	}
-
-	/**
-	 * TreeHelperTest::testGenerate()
-	 *
-	 * @return void
-	 */
 	public function testGenerate() {
 		$tree = $this->Table->find('threaded')->toArray();
 
@@ -137,8 +130,6 @@ TEXT;
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateWithFindAll()
-	 *
 	 * @return void
 	 */
 	public function testGenerateWithFindAll() {
@@ -182,8 +173,6 @@ TEXT;
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateWithDepth()
-	 *
 	 * @return void
 	 */
 	public function testGenerateWithDepth() {
@@ -224,8 +213,6 @@ TEXT;
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateWithSettings()
-	 *
 	 * @return void
 	 */
 	public function testGenerateWithSettings() {
@@ -266,8 +253,6 @@ TEXT;
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateWithMaxDepth()
-	 *
 	 * @return void
 	 */
 	public function testGenerateWithMaxDepth() {
@@ -304,16 +289,12 @@ TEXT;
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateWithAutoPath()
-	 *
 	 * @return void
 	 */
 	public function testGenerateWithAutoPath() {
 		$tree = $this->Table->find('threaded')->toArray();
-		//debug($tree);
 
 		$output = $this->Tree->generate($tree, ['autoPath' => [7, 10]]); // Two-SubA-1
-		//debug($output);
 		$expected = <<<TEXT
 
 <ul>
@@ -397,8 +378,6 @@ TEXT;
 	 * @return void
 	 */
 	public function testGenerateWithAutoPathAndHideUnrelated() {
-		$this->skipIf(true, 'FIXME');
-
 		$data = [
 			['name' => 'Two-SubB', 'parent_id' => 2],
 			['name' => 'Two-SubC', 'parent_id' => 2],
@@ -414,7 +393,6 @@ TEXT;
 		$path = $nodes->extract('id')->toArray();
 
 		$output = $this->Tree->generate($tree, ['autoPath' => [6, 11], 'hideUnrelated' => true, 'treePath' => $path, 'callback' => [$this, '_myCallback']]); // Two-SubA
-		//debug($output);
 
 		$expected = <<<TEXT
 
@@ -457,8 +435,6 @@ TEXT;
 	 * @return void
 	 */
 	public function testGenerateWithAutoPathAndHideUnrelatedAndSiblings() {
-		$this->skipIf(true, 'FIXME');
-
 		$data = [
 			['name' => 'Two-SubB', 'parent_id' => 2],
 			['name' => 'Two-SubC', 'parent_id' => 2],
@@ -469,14 +445,14 @@ TEXT;
 		}
 
 		$tree = $this->Table->find('threaded')->toArray();
-		$id = 6;
+
+		$id = 6; // Two-SubA
 		$nodes = $this->Table->find('path', ['for' => $id]);
 		$path = $nodes->extract('id')->toArray();
 
 		$output = $this->Tree->generate($tree, [
 			'autoPath' => [6, 11], 'hideUnrelated' => true, 'treePath' => $path,
-			'callback' => [$this, '_myCallbackSiblings']]); // Two-SubA
-		//debug($output);
+			'callback' => [$this, '_myCallbackSiblings']]);
 
 		$expected = <<<TEXT
 
@@ -486,7 +462,7 @@ TEXT;
 	<ul>
 		<li class="active">Two-SubA (active)
 		<ul>
-			<li>Two-SubA-1</li>
+			<li>Two-SubA-1</li>	
 		</ul>
 		</li>
 		<li>Two-SubB</li>
@@ -508,7 +484,9 @@ TEXT;
 	 * @return string|null
 	 */
 	public function _myCallback($data) {
-		if (!empty($data['data']['hide'])) {
+		/** @var \Cake\ORM\Entity $entity */
+		$entity = $data['data'];
+		if (!empty($entity['hide'])) {
 			return null;
 		}
 		return $data['data']['name'] . ($data['activePathElement'] ? ' (active)' : '');
@@ -519,18 +497,19 @@ TEXT;
 	 * @return string|null
 	 */
 	public function _myCallbackSiblings($data) {
-		if (!empty($data['data']['hide'])) {
+		/** @var \Cake\ORM\Entity $entity */
+		$entity = $data['data'];
+
+		if (!empty($entity['hide'])) {
 			return null;
 		}
 		if ($data['depth'] == 0 && $data['isSibling']) {
-			return $data['data']['name'] . ' (sibling)';
+			return $entity['name'] . ' (sibling)';
 		}
-		return $data['data']['name'] . ($data['activePathElement'] ? ' (active)' : '');
+		return $entity['name'] . ($data['activePathElement'] ? ' (active)' : '');
 	}
 
 	/**
-	 * TreeHelperTest::testGenerateProductive()
-	 *
 	 * @return void
 	 */
 	public function testGenerateProductive() {
@@ -542,4 +521,76 @@ TEXT;
 		$this->assertTextEquals($expected, $output);
 	}
 
+	/**
+	 * @return void
+	 */
+	public function testGenerateWithEntityUsage() {
+		$data = [
+			['name' => 'Two-SubB', 'parent_id' => 2],
+			['name' => 'Two-SubC', 'parent_id' => 2],
+		];
+		foreach ($data as $row) {
+			$row = new Entity($row);
+			$this->Table->save($row);
+		}
+
+		$tree = $this->Table->find('threaded')->toArray();
+
+		$id = 6;
+		$nodes = $this->Table->find('path', ['for' => $id]);
+		$path = $nodes->extract('id')->toArray();
+
+		$output = $this->Tree->generate($tree, [
+			'autoPath' => [6, 11], 'treePath' => $path,
+			'callback' => [$this, '_myCallbackEntity']]); // Two-SubA
+
+		$expected = <<<TEXT
+
+<ul>
+	<li>One
+	<ul>
+		<li>One-SubA</li>
+	</ul>
+	</li>
+	<li class="active">Two (active)
+	<ul>
+		<li class="active">Two-SubA (active)
+		<ul>
+			<li>Two-SubA-1
+			<ul>
+				<li>Two-SubA-1-1</li>
+			</ul>
+			</li>
+		</ul>
+		</li>
+		<li>Two-SubB</li>
+		<li>Two-SubC</li>
+	</ul>
+	</li>
+	<li>Three</li>
+	<li>Four
+	<ul>
+		<li>Four-SubA</li>
+	</ul>	
+	</li>
+</ul>
+
+TEXT;
+		debug($output);
+		$output = str_replace(["\t", "\r", "\n"], '', $output);
+		$expected = str_replace(["\t", "\r", "\n"], '', $expected);
+		$this->assertTextEquals($expected, $output);
+	}
+
+	/**
+	 * @param array $data
+	 * @return string|null
+	 */
+	public function _myCallbackEntity($data) {
+		/** @var \Cake\ORM\Entity $entity */
+		$entity = $data['data'];
+
+		return h($entity->name) . ($data['activePathElement'] ? ' (active)' : '');
+	}
+
 }

+ 22 - 0
tests/test_app/Model/Table/DataTable.php

@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Model\Table;
+
+use Cake\Database\Schema\TableSchema;
+use Tools\Model\Table\Table;
+
+class DataTable extends Table {
+
+	/**
+	 * @param \Cake\Database\Schema\TableSchema $schema
+	 *
+	 * @return \Cake\Database\Schema\TableSchema
+	 */
+	protected function _initializeSchema(TableSchema $schema) {
+		$schema->setColumnType('data_json', 'json');
+		$schema->setColumnType('data_array', 'array');
+
+		return $schema;
+	}
+
+}