From 9a5106c288519b008e0dfe5e85371701af32c0f3 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Mon, 7 Jul 2025 13:37:19 +0200 Subject: [locations] Cleanup Location class There was a mess of functions which mostly, but not quite, did the same things. Get rid of a couple of them and fix call sites to use alternative ones that also fit the job. While at it, add phpdoc and comments to the remaining functions, trying to clarify what they were designed for. Lastly, the return type of functions that retrieve a location id has been changed from false|int to ?int (nullable types are just nicer). --- modules-available/locations/inc/location.inc.php | 382 ++++++++++++----------- 1 file changed, 193 insertions(+), 189 deletions(-) (limited to 'modules-available/locations/inc/location.inc.php') diff --git a/modules-available/locations/inc/location.inc.php b/modules-available/locations/inc/location.inc.php index 807f8577..d4978068 100644 --- a/modules-available/locations/inc/location.inc.php +++ b/modules-available/locations/inc/location.inc.php @@ -3,28 +3,111 @@ class Location { - private static $flatLocationCache = false; - private static $assocLocationCache = false; - private static $treeCache = false; - private static $subnetMapCache = false; + /** @var ?array */ + private static $flatLocationCache = null; + /** @var ?array */ + private static $assocLocationCache = null; - public static function getTree(): array + /** + * Get a nested array of locations. Each element in the array represents a top-level + * location, and locations with have child-nodes will have a 'children' key that again + * contains an array of location elements, with 'children' as appropriate, and so on. + * If you pass one or more startLocationIds, the tree will be cut down to only contain + * the locations given (plus any subtrees they have). + * @return array + */ + public static function getTree(int ...$startLocationId): array { - if (self::$treeCache === false) { - self::$treeCache = self::queryLocations(); - self::$treeCache = self::buildTree(self::$treeCache); + static $treeCache = null; + if ($treeCache === null) { + $treeCache = self::buildTree(self::queryLocations()); } - return self::$treeCache; + if (empty($startLocationId) || in_array(0, $startLocationId)) + return $treeCache; + // Subtree, search + return self::filterTreeByLocations($treeCache, $startLocationId); } - public static function queryLocations(): array + /** + * Return a filtered tree that only contains the locations present in $idList, plus their respective + * child locations in the 'children' array key, if applicable. Top level elements will only be those + * contained in $idList, but not every location given in $idList will be a top level element, as they + * might already be present in one of the nested 'children' arrays. + * @param array $locationTree part of tree to search in + * @param array $idList list of wanted ids + * @return array filtered tree + */ + private static function filterTreeByLocations(array $locationTree, array $idList): array { - $res = Database::simpleQuery("SELECT locationid, parentlocationid, locationname FROM location"); - $rows = array(); - foreach ($res as $row) { - $rows[] = $row; + $ret = []; + foreach ($locationTree as $location) { + if (in_array($location['locationid'], $idList)) { + $ret[] = $location; + } elseif (!empty($location['children'])) { + $ret = array_merge($ret, self::filterTreeByLocations($location['children'], $idList)); + } + } + return $ret; + } + + /** + * @return array + */ + private static function buildTree(array $elements, int $parentId = 0): array + { + $branch = array(); + foreach ($elements as $lid => $element) { + $lid = (int)$lid; + if ($lid === 0 || $lid === $parentId) + continue; + if ($element['parentlocationid'] === $parentId) { + $children = self::buildTree($elements, $lid); + if (!empty($children)) { + $element['children'] = $children; + } + $element['locationid'] = $lid; + $branch[] = $element; + } + } + ArrayUtil::sortByColumn($branch, 'locationname'); + return $branch; + } + + private static function queryLocations(): array + { + static $locationDbCache = null; + if ($locationDbCache === null) { + $locationDbCache = Database::queryIndexedList("SELECT locationid, parentlocationid, locationname FROM location"); + foreach ($locationDbCache as &$loc) { + $loc['parentlocationid'] = (int)$loc['parentlocationid']; + } + } + return $locationDbCache; + } + + /** + * Get all location ids starting from the specified location id. + * If $startId is 0, it will return all available location ids. + * If $withStart is true, the starting location id will be included in the result. + * + * @param int $startId The location id to start from (default is 0 to get all location ids). + * @param bool $withStart True to include the starting location id in the result (default is false). + * @return int[] An array of location ids, including the starting location id if requested. + */ + public static function getAllLocationIds(int $startId = 0, bool $withStart = false): array + { + if ($startId === 0) { + $locs = array_keys(self::queryLocations()); + } else { + $locs = self::getLocationsAssoc(); + if (!isset($locs[$startId])) + return []; + $locs = $locs[$startId]['children'] ?? []; + } + if ($withStart) { + $locs[] = $startId; } - return $rows; + return $locs; } /** @@ -43,79 +126,79 @@ class Location */ public static function getName(int $locationId) { - if (self::$assocLocationCache === false) { - self::getLocationsAssoc(); - } - if (!isset(self::$assocLocationCache[$locationId])) - return false; - return self::$assocLocationCache[$locationId]['locationname']; + $locs = self::queryLocations(); + return $locs[$locationId]['locationname'] ?? false; } /** * Get all the names of the given location and its parents, up * to the root element. Array keys will be locationids, value the names. - * @return array|false locations, from furthest to nearest or false if locationId doesn't exist + * @return ?array locations, from furthest to nearest or false if locationId doesn't exist */ - public static function getNameChain(int $locationId) + public static function getNameChain(int $locationId): ?array { - if (self::$assocLocationCache === false) { - self::getLocationsAssoc(); - } - if (!isset(self::$assocLocationCache[$locationId])) - return false; - $ret = array(); - while (isset(self::$assocLocationCache[$locationId])) { - $ret[$locationId] = self::$assocLocationCache[$locationId]['locationname']; - $locationId = self::$assocLocationCache[$locationId]['parentlocationid']; + $locs = self::queryLocations(); + $ret = []; + while (isset($locs[$locationId])) { + $ret[$locationId] = $locs[$locationId]['locationname']; + $locationId = $locs[$locationId]['parentlocationid']; } + if (empty($ret)) + return null; return array_reverse($ret, true); } - public static function getLocationsAssoc() + /** + * Get associative array of locations. Mostly used for working with locations, in contrast to ::getLocations() + * @return array All the locations + */ + public static function getLocationsAssoc(): array { - if (self::$assocLocationCache === false) { - $rows = self::getTree(); - self::$assocLocationCache = self::flattenTreeAssoc($rows); + if (self::$assocLocationCache === null) { + self::$assocLocationCache = self::flattenTreeAssoc(self::getTree()); } return self::$assocLocationCache; } - private static function flattenTreeAssoc($tree, $parents = array(), $depth = 0): array + /** + * @return array All the locations + */ + private static function flattenTreeAssoc(array $tree, array $parents = [], int $depth = 0): array { - if ($depth > 20) { - ErrorHandler::traceError('Recursive location definition detected at ' . print_r($tree, true)); - } - $output = array(); + $output = []; foreach ($tree as $node) { - $cc = empty($node['children']) ? array() : array_map(function ($item) { return (int)$item['locationid']; }, $node['children']); - $output[(int)$node['locationid']] = array( - 'locationid' => (int)$node['locationid'], - 'parentlocationid' => (int)$node['parentlocationid'], + $cc = empty($node['children']) ? [] : array_column($node['children'], 'locationid'); + $output[$node['locationid']] = [ + 'locationid' => $node['locationid'], + 'parentlocationid' => $node['parentlocationid'], 'parents' => $parents, 'children' => $cc, 'directchildren' => $cc, 'locationname' => $node['locationname'], 'depth' => $depth, - 'isleaf' => true, - ); + 'isleaf' => empty($cc), + ]; if (!empty($node['children'])) { - $childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, array((int)$node['locationid'])), $depth + 1); - $output[(int)$node['locationid']]['children'] = array_merge($output[(int)$node['locationid']]['children'], + $childNodes = self::flattenTreeAssoc($node['children'], array_merge($parents, [$node['locationid']]), $depth + 1); + $output[$node['locationid']]['children'] = array_merge($output[$node['locationid']]['children'], array_reduce($childNodes, function ($carry, $item) { return array_merge($carry, $item['children']); - }, array())); + }, [])); $output += $childNodes; } } - foreach ($output as &$entry) { - if (!isset($output[$entry['parentlocationid']])) - continue; - $output[$entry['parentlocationid']]['isleaf'] = false; - } return $output; } + private static function buildFlatTree(): void + { + if (self::$flatLocationCache === null) { + self::$flatLocationCache = self::flattenTree(self::getTree()); + } + } + /** + * get locations as flat array - mostly used for rendering a