Skip to content

Commit 55929c0

Browse files
le0mStefano Vavassori
and
Stefano Vavassori
authored
Tree slugs and new tree_paths endpoint (#2132)
* feat: added slug property to trees table and controller for slug paths * chore: add tests for tree slugs * fix: retrieve correct object based on path using slug * feat: add tests to treePathsController in TreePathsControllerTest.php * feature: reach 100% coverage for treePathsController test * refactor: move path details to treesTable.php * refactor: move path tests to treesTable.php + 100% coverage * fix: phpcs --------- Co-authored-by: Stefano Vavassori <[email protected]>
1 parent c810c59 commit 55929c0

7 files changed

+317
-0
lines changed

config/routes.php

+6
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,12 @@ function (RouteBuilder $routes) use ($modelingControllers, $resourcesRoutes) {
244244
['controller' => 'Trees', 'action' => 'index', '_method' => 'GET'],
245245
['_name' => 'trees:index']
246246
);
247+
// Tree paths.
248+
$routes->connect(
249+
'/tree_paths/**',
250+
['controller' => 'TreePaths', 'action' => 'index', '_method' => 'GET'],
251+
['_name' => 'tree_paths:index']
252+
);
247253

248254
// Upload file and create object.
249255
$routes->connect(
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* BEdita, API-first content management framework
6+
* Copyright 2025 Atlas Srl, Chialab Srl
7+
*
8+
* This file is part of BEdita: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14+
*/
15+
namespace BEdita\API\Controller;
16+
17+
/**
18+
* Controller for `/tree_paths` endpoint.
19+
*
20+
* @since 5.36.12
21+
*/
22+
class TreePathsController extends TreesController
23+
{
24+
/**
25+
* Path information with ID, object type and slug of each object
26+
* Associative array having keys:
27+
* - 'ids': ID path list
28+
* - 'slugs': slug path list
29+
* - 'types': object types id list
30+
*
31+
* @var array
32+
*/
33+
protected $pathInfo = [
34+
'ids' => [],
35+
'slugs' => [],
36+
'types' => [],
37+
];
38+
39+
/**
40+
* Display object on a given path
41+
*
42+
* @param string $path Trees path
43+
* @return \Cake\Http\Response|null
44+
*/
45+
public function index(string $path)
46+
{
47+
$this->request->allowMethod(['get']);
48+
49+
// populate idList, unameList
50+
$this->pathInfo = $this->Objects->TreeNodes->getPathInfo($path);
51+
52+
$this->loadTreesNode();
53+
$parents = $this->parents();
54+
55+
$ids = array_values((array)$this->pathInfo['ids']);
56+
$entity = $this->loadObject(end($ids));
57+
58+
$this->checkPath($entity, $parents);
59+
60+
$entity->set('slug_path', sprintf('/%s', implode('/', $this->pathInfo['slugs'])));
61+
$entity->setAccess('slug_path', false);
62+
$entity->set('menu', (bool)$this->treesNode->get('menu'));
63+
64+
$this->set('_fields', $this->request->getQuery('fields', []));
65+
$this->set(compact('entity'));
66+
$this->setSerialize(['entity']);
67+
68+
return null;
69+
}
70+
}

tests/IntegrationTest/ChildrenRelationshipTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public function testChildrenMeta()
184184
'menu' => true,
185185
'canonical' => true,
186186
'params' => null,
187+
'slug' => 'gustavo-supporto-profile-4',
187188
];
188189
static::assertEquals($expected, Hash::get($result, 'data.0.meta.relation'));
189190
}

tests/IntegrationTest/ParentsRelationshipTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ public function testParentsMeta()
334334
'menu' => true,
335335
'canonical' => true,
336336
'params' => null,
337+
'slug' => 'gustavo-supporto-profile-4',
337338
];
338339
static::assertEquals($expected, Hash::get($result, 'data.0.meta.relation'));
339340

@@ -348,6 +349,7 @@ public function testParentsMeta()
348349
'menu' => true,
349350
'canonical' => true,
350351
'params' => null,
352+
'slug' => 'sub-folder-12',
351353
];
352354
static::assertEquals($expected, Hash::get($result, 'data.meta.relation'));
353355
}

tests/TestCase/Controller/FoldersControllerTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ public function testIndex()
112112
'created_by' => 1,
113113
'modified_by' => 1,
114114
'path' => '/11',
115+
'slug_path' => [[
116+
'id' => 11,
117+
'menu' => false,
118+
'params' => null,
119+
'slug' => 'root-folder-11',
120+
]],
115121
],
116122
'links' => [
117123
'self' => 'http://api.example.com/folders/11',
@@ -160,6 +166,20 @@ public function testIndex()
160166
'created_by' => 1,
161167
'modified_by' => 1,
162168
'path' => '/11/12',
169+
'slug_path' => [
170+
[
171+
'id' => 11,
172+
'menu' => false,
173+
'params' => null,
174+
'slug' => 'root-folder-11',
175+
],
176+
[
177+
'id' => 12,
178+
'menu' => true,
179+
'params' => null,
180+
'slug' => 'sub-folder-12',
181+
],
182+
],
163183
],
164184
'links' => [
165185
'self' => 'http://api.example.com/folders/12',
@@ -208,6 +228,12 @@ public function testIndex()
208228
'created_by' => 1,
209229
'modified_by' => 1,
210230
'path' => '/13',
231+
'slug_path' => [[
232+
'id' => 13,
233+
'menu' => true,
234+
'params' => null,
235+
'slug' => 'another-root-folder-13',
236+
]],
211237
],
212238
'links' => [
213239
'self' => 'http://api.example.com/folders/13',
@@ -327,6 +353,12 @@ public function testSingle()
327353
'created_by' => 1,
328354
'modified_by' => 1,
329355
'path' => '/11',
356+
'slug_path' => [[
357+
'id' => 11,
358+
'menu' => false,
359+
'params' => null,
360+
'slug' => 'root-folder-11',
361+
]],
330362
],
331363
'relationships' => [
332364
'children' => [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* BEdita, API-first content management framework
6+
* Copyright 2025 ChannelWeb Srl, Chialab Srl
7+
*
8+
* This file is part of BEdita: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14+
*/
15+
16+
namespace BEdita\API\Test\TestCase\Controller;
17+
18+
use BEdita\API\TestSuite\IntegrationTestCase;
19+
use Cake\ORM\TableRegistry;
20+
21+
/**
22+
* @coversDefaultClass \BEdita\API\Controller\TreePathsController
23+
*/
24+
class TreePathsControllerTest extends IntegrationTestCase
25+
{
26+
/**
27+
* Test `index` with a valid slug.
28+
*
29+
* @return void
30+
* @covers ::index()
31+
* @covers ::pathDetails()
32+
*/
33+
public function testIndexValidSlugPath(): void
34+
{
35+
$this->configRequestHeaders();
36+
$this->get('/tree_paths/root-folder-11/sub-folder-12');
37+
38+
$this->assertResponseCode(200);
39+
$this->assertContentType('application/vnd.api+json');
40+
41+
$response = json_decode((string)$this->_response->getBody(), true);
42+
43+
$expected = [
44+
'data' => [
45+
'id' => '12',
46+
'type' => 'folders',
47+
'attributes' => [
48+
'status' => 'on',
49+
'uname' => 'sub-folder',
50+
'title' => 'Sub Folder',
51+
'description' => 'sub folder of root folder',
52+
'body' => null,
53+
'extra' => null,
54+
'lang' => 'en',
55+
'publish_start' => null,
56+
'publish_end' => null,
57+
'children_order' => null,
58+
'menu' => true,
59+
],
60+
'meta' => [
61+
'locked' => false,
62+
'path' => '/11/12',
63+
'slug_path' => [
64+
[
65+
'id' => 11,
66+
'menu' => false,
67+
'params' => null,
68+
'slug' => 'root-folder-11',
69+
],
70+
[
71+
'id' => 12,
72+
'menu' => true,
73+
'params' => null,
74+
'slug' => 'sub-folder-12',
75+
],
76+
],
77+
'published' => null,
78+
'created_by' => 1,
79+
'modified_by' => 1,
80+
'created' => '2018-01-31T07:09:23+00:00',
81+
'modified' => '2018-01-31T08:30:00+00:00',
82+
],
83+
'relationships' => [
84+
'translations' => [
85+
'links' => [
86+
'related' => 'http://api.example.com/folders/12/translations',
87+
'self' => 'http://api.example.com/folders/12/relationships/translations',
88+
],
89+
],
90+
'children' => [
91+
'links' => [
92+
'related' => 'http://api.example.com/folders/12/children',
93+
'self' => 'http://api.example.com/folders/12/relationships/children',
94+
],
95+
],
96+
'parent' => [
97+
'links' => [
98+
'related' => 'http://api.example.com/folders/12/parent',
99+
'self' => 'http://api.example.com/folders/12/relationships/parent',
100+
],
101+
],
102+
],
103+
],
104+
'links' => [
105+
'self' => 'http://api.example.com/tree_paths',
106+
'home' => 'http://api.example.com/home',
107+
],
108+
'meta' => [
109+
'schema' => [
110+
'folders' => [
111+
'$id' => 'http://api.example.com/model/schema/folders',
112+
'revision' => '3048758948',
113+
],
114+
],
115+
],
116+
];
117+
118+
static::assertEquals($expected, $response);
119+
}
120+
121+
/**
122+
* Test `index` with an invalid slug.
123+
*
124+
* @return void
125+
* @covers ::index()
126+
*/
127+
public function testIndexInvalidSlugPath(): void
128+
{
129+
$this->configRequestHeaders();
130+
$this->get('/tree_paths/pippo/pluto');
131+
132+
$this->assertResponseCode(404);
133+
$this->assertContentType('application/vnd.api+json');
134+
135+
$response = json_decode((string)$this->_response->getBody(), true);
136+
137+
static::assertEquals($response['error']['title'], 'Invalid path');
138+
}
139+
140+
/**
141+
* Verify that two object with the same slug are reachable with different paths.
142+
*
143+
* @return void
144+
* @covers ::index()
145+
*/
146+
public function testObjectsWithSameSlugDifferentPaths(): void
147+
{
148+
$treesTable = TableRegistry::getTableLocator()->get('Trees');
149+
150+
// Create a new object with the same slug but different paths
151+
$newTreeNode = $treesTable->newEntity([
152+
'object_id' => 14,
153+
'parent_id' => 13, // Another-root-folder-13
154+
'root_id' => 13,
155+
'parent_node_id' => 5,
156+
'tree_left' => 10,
157+
'tree_right' => 11,
158+
'depth_level' => 1,
159+
'menu' => 1,
160+
'canonical' => 1,
161+
'slug' => 'gustavo-supporto-profile-4',
162+
]);
163+
164+
$treesTable->saveOrFail($newTreeNode);
165+
166+
$this->configRequestHeaders();
167+
// First object in "another-root-folder-13/gustavo-supporto-profile-4"
168+
$this->get('/tree_paths/another-root-folder-13/gustavo-supporto-profile-4');
169+
$this->assertResponseCode(200);
170+
$this->assertContentType('application/vnd.api+json');
171+
$response1 = json_decode((string)$this->_response->getBody(), true);
172+
173+
$this->configRequestHeaders();
174+
// Second object in "root-folder-11/sub-folder-12/gustavo-supporto-profile-4"
175+
$this->get('/tree_paths/root-folder-11/sub-folder-12/gustavo-supporto-profile-4');
176+
$this->assertResponseCode(200);
177+
$this->assertContentType('application/vnd.api+json');
178+
$response2 = json_decode((string)$this->_response->getBody(), true);
179+
180+
static::assertNotEquals(
181+
$response1['data']['id'],
182+
$response2['data']['id'],
183+
'Object IDS must be different even if they have the same slug'
184+
);
185+
186+
static::assertNotEquals(
187+
$response1['data']['meta']['extra']['slug_path'],
188+
$response2['data']['meta']['extra']['slug_path'],
189+
'Paths must me different for the same slug.'
190+
);
191+
}
192+
}

tests/TestCase/Controller/TreesControllerTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ public function testIndex(): void
7272
'extra' => [
7373
'uname_path' => '/root-folder/sub-folder',
7474
],
75+
'slug_path' => [
76+
[
77+
'id' => 11,
78+
'menu' => false,
79+
'params' => null,
80+
'slug' => 'root-folder-11',
81+
],
82+
[
83+
'id' => 12,
84+
'menu' => true,
85+
'params' => null,
86+
'slug' => 'sub-folder-12',
87+
],
88+
],
7589
],
7690
'relationships' => [
7791
'children' => [

0 commit comments

Comments
 (0)