diff --git a/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php b/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php index c11c85c77a0..2a663216efd 100644 --- a/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php @@ -630,7 +630,7 @@ public function save() 'cssClassName' => $this->cssClassName, 'showHeader' => $this->showHeader, 'isDisabled' => $this->isDisabled ? 1 : 0, - 'linkPageID' => $this->linkPageID, + 'linkPageID' => $this->linkPageID ?: null, 'linkPageObjectID' => $this->linkPageObjectID ?: 0, 'externalURL' => $this->externalURL, 'identifier' => '', diff --git a/wcfsetup/install/files/lib/acp/form/BoxEditForm.class.php b/wcfsetup/install/files/lib/acp/form/BoxEditForm.class.php index f22011d6dbd..7112f1f27aa 100644 --- a/wcfsetup/install/files/lib/acp/form/BoxEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/BoxEditForm.class.php @@ -119,7 +119,7 @@ public function save() 'cssClassName' => $this->cssClassName, 'showHeader' => $this->showHeader, 'isDisabled' => $this->isDisabled ? 1 : 0, - 'linkPageID' => $this->linkPageID, + 'linkPageID' => $this->linkPageID ?: null, 'linkPageObjectID' => $this->linkPageObjectID ?: 0, 'externalURL' => $this->externalURL, 'invertPermissions' => $this->invertPermissions, diff --git a/wcfsetup/install/files/lib/acp/form/LanguageMultilingualismForm.class.php b/wcfsetup/install/files/lib/acp/form/LanguageMultilingualismForm.class.php index 991a382cb04..3cc52c05dfd 100644 --- a/wcfsetup/install/files/lib/acp/form/LanguageMultilingualismForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LanguageMultilingualismForm.class.php @@ -5,7 +5,7 @@ use wcf\data\language\Language; use wcf\data\language\LanguageEditor; use wcf\form\AbstractForm; -use wcf\system\cache\builder\LanguageCacheBuilder; +use wcf\system\cache\eager\LanguageCache; use wcf\system\exception\UserInputException; use wcf\system\language\LanguageFactory; use wcf\system\WCF; @@ -116,7 +116,7 @@ public function save() LanguageEditor::enableMultilingualism(($this->enable == 1 ? $this->languageIDs : [])); // clear cache - LanguageCacheBuilder::getInstance()->reset(); + (new LanguageCache())->rebuild(); $this->saved(); // show success message diff --git a/wcfsetup/install/files/lib/data/language/LanguageEditor.class.php b/wcfsetup/install/files/lib/data/language/LanguageEditor.class.php index 02c077df45e..d036b1aed3c 100644 --- a/wcfsetup/install/files/lib/data/language/LanguageEditor.class.php +++ b/wcfsetup/install/files/lib/data/language/LanguageEditor.class.php @@ -10,7 +10,7 @@ use wcf\data\language\item\LanguageItemList; use wcf\data\page\PageEditor; use wcf\event\language\LanguageContentCopying; -use wcf\system\cache\builder\LanguageCacheBuilder; +use wcf\system\cache\eager\LanguageCache; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\event\EventHandler; use wcf\system\exception\SystemException; @@ -932,7 +932,7 @@ public function setAsDefault() $statement->execute([0]); // set current language as default language - $this->update(['isDefault' => 1]); + $this->update(['isDefault' => 1, 'isDisabled' => 0]); $this->clearCache(); } @@ -944,7 +944,7 @@ public function setAsDefault() */ public function clearCache() { - LanguageCacheBuilder::getInstance()->reset(); + (new LanguageCache())->rebuild(); } /** diff --git a/wcfsetup/install/files/lib/system/WCF.class.php b/wcfsetup/install/files/lib/system/WCF.class.php index a0baee82d54..177748f0ca3 100644 --- a/wcfsetup/install/files/lib/system/WCF.class.php +++ b/wcfsetup/install/files/lib/system/WCF.class.php @@ -12,13 +12,12 @@ use wcf\system\application\IApplication; use wcf\system\benchmark\Benchmark; use wcf\system\box\BoxHandler; -use wcf\system\cache\builder\CoreObjectCacheBuilder; use wcf\system\cache\builder\PackageUpdateCacheBuilder; +use wcf\system\cache\eager\CoreObjectCache; use wcf\system\database\MySQLDatabase; use wcf\system\event\EventHandler; use wcf\system\exception\ErrorException; use wcf\system\exception\IPrintableException; -use wcf\system\exception\ParentClassException; use wcf\system\exception\SystemException; use wcf\system\language\LanguageFactory; use wcf\system\package\command\RebuildBootstrapper; @@ -140,9 +139,10 @@ class WCF /** * list of cached core objects - * @var string[] + * + * @var array> */ - protected static $coreObjectCache = []; + protected static array $coreObjectCache; /** * database object @@ -752,7 +752,7 @@ protected function initCoreObjects(): void return; } - self::$coreObjectCache = CoreObjectCacheBuilder::getInstance()->getData(); + self::$coreObjectCache = (new CoreObjectCache())->getCache(); } /** @@ -919,10 +919,6 @@ final public static function __callStatic(string $name, array $arguments) } if (\class_exists($objectName)) { - if (!\is_subclass_of($objectName, SingletonFactory::class)) { - throw new ParentClassException($objectName, SingletonFactory::class); - } - self::$coreObject[$className] = \call_user_func([$objectName, 'getInstance']); return self::$coreObject[$className]; @@ -934,7 +930,7 @@ final public static function __callStatic(string $name, array $arguments) /** * Searches for cached core object definition. * - * @return string|null + * @return class-string|null */ final protected static function getCoreObject(string $className) { diff --git a/wcfsetup/install/files/lib/system/WCFSetup.class.php b/wcfsetup/install/files/lib/system/WCFSetup.class.php index f75c87e8c32..00508fb9aa4 100644 --- a/wcfsetup/install/files/lib/system/WCFSetup.class.php +++ b/wcfsetup/install/files/lib/system/WCFSetup.class.php @@ -10,7 +10,7 @@ use wcf\data\package\installation\queue\PackageInstallationQueueEditor; use wcf\data\user\User; use wcf\data\user\UserAction; -use wcf\system\cache\builder\LanguageCacheBuilder; +use wcf\system\cache\eager\LanguageCache; use wcf\system\database\Database; use wcf\system\database\exception\DatabaseException; use wcf\system\database\MySQLDatabase; @@ -779,7 +779,7 @@ protected function installLanguage(): ResponseInterface LanguageFactory::getInstance()->makeDefault($language->languageID); // rebuild language cache - LanguageCacheBuilder::getInstance()->reset(); + (new LanguageCache())->rebuild(); return $this->gotoNextStep('createUser'); } diff --git a/wcfsetup/install/files/lib/system/background/job/TolerantCacheRebuildBackgroundJob.class.php b/wcfsetup/install/files/lib/system/background/job/TolerantCacheRebuildBackgroundJob.class.php new file mode 100644 index 00000000000..be0d194a9f1 --- /dev/null +++ b/wcfsetup/install/files/lib/system/background/job/TolerantCacheRebuildBackgroundJob.class.php @@ -0,0 +1,60 @@ + + * @since 6.2 + */ +final class TolerantCacheRebuildBackgroundJob extends AbstractUniqueBackgroundJob +{ + public function __construct( + /** @var class-string */ + public readonly string $cacheClass, + /** @var array */ + public readonly array $parameters = [] + ) { + } + + public function identifier(): string + { + $identifier = $this->cacheClass; + if (!empty($this->parameters)) { + $identifier .= '-' . CacheHandler::getInstance()->getCacheIndex($this->parameters); + } + + return $identifier; + } + + #[\Override] + public function newInstance(): static + { + return new TolerantCacheRebuildBackgroundJob($this->cacheClass, $this->parameters); + } + + #[\Override] + public function queueAgain(): bool + { + return false; + } + + #[\Override] + public function perform() + { + if (!\class_exists($this->cacheClass)) { + return; + } + + $asyncCache = new $this->cacheClass(...$this->parameters); + \assert($asyncCache instanceof AbstractTolerantCache); + + $asyncCache->rebuild(); + } +} diff --git a/wcfsetup/install/files/lib/system/box/WhoWasOnlineBoxController.class.php b/wcfsetup/install/files/lib/system/box/WhoWasOnlineBoxController.class.php index aa3d8aa3204..0ee2f822a68 100644 --- a/wcfsetup/install/files/lib/system/box/WhoWasOnlineBoxController.class.php +++ b/wcfsetup/install/files/lib/system/box/WhoWasOnlineBoxController.class.php @@ -7,8 +7,8 @@ use wcf\data\user\online\UserOnline; use wcf\data\user\online\UsersOnlineList; use wcf\data\user\UserProfile; -use wcf\system\cache\builder\WhoWasOnlineCacheBuilder; use wcf\system\cache\runtime\UserProfileRuntimeCache; +use wcf\system\cache\tolerant\WhoWasOnlineCache; use wcf\system\event\EventHandler; use wcf\system\WCF; use wcf\util\DateUtil; @@ -100,14 +100,9 @@ protected function readObjects() { EventHandler::getInstance()->fireAction($this, 'readObjects'); - $userIDs = WhoWasOnlineCacheBuilder::getInstance()->getData(); + $userIDs = (new WhoWasOnlineCache())->getCache(); if (!empty($userIDs)) { - if (WCF::getUser()->userID && !\in_array(WCF::getUser()->userID, $userIDs)) { - // current user is missing in cache -> reset cache - WhoWasOnlineCacheBuilder::getInstance()->reset(); - } - $this->users = \array_filter( UserProfileRuntimeCache::getInstance()->getObjects($userIDs), static function ($user) { diff --git a/wcfsetup/install/files/lib/system/cache/builder/AbstractLegacyCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/AbstractLegacyCacheBuilder.class.php new file mode 100644 index 00000000000..29671201066 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/AbstractLegacyCacheBuilder.class.php @@ -0,0 +1,58 @@ + + * + * @since 6.2 + * @deprecated 6.2 + */ +abstract class AbstractLegacyCacheBuilder extends SingletonFactory implements ICacheBuilder +{ + /** + * @var array> + */ + private array $cache = []; + + #[\Override] + public function getData(array $parameters = [], $arrayIndex = '') + { + $index = CacheHandler::getInstance()->getCacheIndex($parameters); + if (isset($this->cache[$index])) { + $cache = $this->cache[$index]; + } else { + $cache = $this->rebuild($parameters); + $this->cache[$index] = $cache; + } + + if (!empty($arrayIndex)) { + if (!\array_key_exists($arrayIndex, $cache)) { + throw new SystemException("array index '" . $arrayIndex . "' does not exist in cache resource"); + } + + return $cache[$arrayIndex]; + } + + return $cache; + } + + /** + * Rebuilds cache for current resource. + */ + abstract protected function rebuild(array $parameters): array; + + #[\Override] + final public function getMaxLifetime() + { + return 0; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/CoreObjectCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/CoreObjectCacheBuilder.class.php index c851abdd72b..ceb97819a41 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/CoreObjectCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/CoreObjectCacheBuilder.class.php @@ -2,34 +2,28 @@ namespace wcf\system\cache\builder; -use wcf\data\core\object\CoreObjectList; +use wcf\system\cache\eager\CoreObjectCache; /** * Caches the core objects. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `CoreObjectCache` instead */ -class CoreObjectCacheBuilder extends AbstractCacheBuilder +class CoreObjectCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - public function rebuild(array $parameters) + #[\Override] + public function reset(array $parameters = []) { - $data = []; - - $coreObjectList = new CoreObjectList(); - $coreObjectList->readObjects(); - $coreObjects = $coreObjectList->getObjects(); - - foreach ($coreObjects as $coreObject) { - $tmp = \explode('\\', $coreObject->objectName); - $className = \array_pop($tmp); - $data[$className] = $coreObject->objectName; - } + (new CoreObjectCache())->rebuild(); + } - return $data; + #[\Override] + public function rebuild(array $parameters): array + { + return (new CoreObjectCache())->getCache(); } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/LanguageCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/LanguageCacheBuilder.class.php index abc3de6c8c2..69a3083eb63 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/LanguageCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/LanguageCacheBuilder.class.php @@ -2,19 +2,17 @@ namespace wcf\system\cache\builder; -use wcf\data\DatabaseObject; use wcf\data\language\category\LanguageCategory; -use wcf\data\language\category\LanguageCategoryList; use wcf\data\language\Language; -use wcf\data\language\LanguageList; +use wcf\system\cache\eager\LanguageCache; /** * Caches languages and the id of the default language. * - * @author Marcel Werk - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Marcel Werk + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License - * @phpstan-type LanguageCache array{ + * @phpstan-type LanguageCacheData array{ * codes: array, * countryCodes: array, * languages: array, @@ -23,57 +21,30 @@ * categoryIDs: array, * multilingualismEnabled: bool, * } + * + * @deprecated 6.2 use `LanguageCache` instead */ -class LanguageCacheBuilder extends AbstractCacheBuilder +class LanguageCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - public function rebuild(array $parameters) + #[\Override] + public function reset(array $parameters = []) { - $data = [ - 'codes' => [], - 'countryCodes' => [], - 'languages' => [], - 'default' => 0, - 'categories' => [], - 'categoryIDs' => [], - 'multilingualismEnabled' => false, - ]; - - // get languages - $languageList = new LanguageList(); - $languageList->getConditionBuilder()->add('language.isDisabled = ?', [0]); - $languageList->readObjects(); - $data['languages'] = $languageList->getObjects(); - foreach ($languageList->getObjects() as $language) { - // default language - if ($language->isDefault) { - $data['default'] = $language->languageID; - } - - // multilingualism - if ($language->hasContent) { - $data['multilingualismEnabled'] = true; - } - - // language code to language id - $data['codes'][$language->languageCode] = $language->languageID; - - // country code to language id - $data['countryCode'][$language->languageID] = $language->countryCode; - } - - DatabaseObject::sort($data['languages'], 'languageName'); - - // get language categories - $languageCategoryList = new LanguageCategoryList(); - $languageCategoryList->readObjects(); - foreach ($languageCategoryList->getObjects() as $languageCategory) { - $data['categories'][$languageCategory->languageCategory] = $languageCategory; - $data['categoryIDs'][$languageCategory->languageCategoryID] = $languageCategory->languageCategory; - } + (new LanguageCache())->rebuild(); + } - return $data; + #[\Override] + public function rebuild(array $parameters): array + { + $cacheData = (new LanguageCache())->getCache(); + + return [ + 'codes' => $cacheData->codes, + 'countryCodes' => $cacheData->countryCodes, + 'languages' => $cacheData->languages, + 'default' => $cacheData->default, + 'categories' => $cacheData->categories, + 'categoryIDs' => $cacheData->categoryIDs, + 'multilingualismEnabled' => $cacheData->multilingualismEnabled, + ]; } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/UserBirthdayCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/UserBirthdayCacheBuilder.class.php index 728949fc413..ec6881fbd41 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/UserBirthdayCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/UserBirthdayCacheBuilder.class.php @@ -2,8 +2,7 @@ namespace wcf\system\cache\builder; -use wcf\data\user\User; -use wcf\system\WCF; +use wcf\system\cache\tolerant\UserBirthdayCache; /** * Caches user birthdays (one cache file per month). @@ -11,40 +10,25 @@ * @author Marcel Werk * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `UserBirthdayCache` instead */ -class UserBirthdayCacheBuilder extends AbstractCacheBuilder +class UserBirthdayCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - protected $maxLifetime = 3600; - - /** - * @inheritDoc - */ - protected function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $userOptionID = User::getUserOptionID('birthday'); - if ($userOptionID === null) { - // birthday profile field missing; skip - return []; + $cache = []; + foreach ((new UserBirthdayCache($parameters['month']))->getCache() as $day => $userIDs) { + $cache[\sprintf("%02d-%02d", $parameters['month'], $day)] = $userIDs; } - $data = []; - $birthday = 'userOption' . $userOptionID; - $sql = "SELECT userID, " . $birthday . " - FROM wcf1_user_option_value - WHERE " . $birthday . " LIKE ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute(['%-' . ($parameters['month'] < 10 ? '0' : '') . $parameters['month'] . '-%']); - while ($row = $statement->fetchArray()) { - [, $month, $day] = \explode('-', $row[$birthday]); - if (!isset($data[$month . '-' . $day])) { - $data[$month . '-' . $day] = []; - } - $data[$month . '-' . $day][] = $row['userID']; - } + return $cache; + } - return $data; + #[\Override] + public function reset(array $parameters = []) + { + (new UserBirthdayCache($parameters['month']))->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/WhoWasOnlineCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/WhoWasOnlineCacheBuilder.class.php index d498afcb509..5be0c6eced6 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/WhoWasOnlineCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/WhoWasOnlineCacheBuilder.class.php @@ -2,44 +2,28 @@ namespace wcf\system\cache\builder; -use wcf\system\WCF; +use wcf\system\cache\tolerant\WhoWasOnlineCache; /** * Caches a list of users that visited the website in last 24 hours. * - * @author Marcel Werk + * @author Olaf Braun, Marcel Werk * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `WhoWasOnlineCache` instead */ -class WhoWasOnlineCacheBuilder extends AbstractCacheBuilder +class WhoWasOnlineCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - protected $maxLifetime = 600; - - /** - * @inheritDoc - */ - protected function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $userIDs = []; - $sql = "( - SELECT userID - FROM wcf1_user - WHERE lastActivityTime > ? - ) UNION ( - SELECT userID - FROM wcf1_session - WHERE userID IS NOT NULL - AND lastActivityTime > ? - )"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([TIME_NOW - 86400, TIME_NOW - USER_ONLINE_TIMEOUT]); - while ($userID = $statement->fetchColumn()) { - $userIDs[] = $userID; - } + return (new WhoWasOnlineCache())->getCache(); + } - return $userIDs; + #[\Override] + public function reset(array $parameters = []) + { + (new WhoWasOnlineCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/AbstractEagerCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/AbstractEagerCache.class.php new file mode 100644 index 00000000000..26f3213ac90 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/AbstractEagerCache.class.php @@ -0,0 +1,92 @@ + + * @since 6.2 + * + * @template T of array|object + */ +abstract class AbstractEagerCache +{ + /** + * @var array + */ + private static array $caches = []; + private string $cacheName; + + /** + * Returns the cache. + * + * @return T + */ + final public function getCache(): array|object + { + $key = $this->getCacheKey(); + + if (!\array_key_exists($key, AbstractEagerCache::$caches)) { + $cache = CacheHandler::getInstance()->getCacheSource()->get($key, 0); + if ($cache === null) { + $this->rebuild(); + } else { + AbstractEagerCache::$caches[$key] = $cache; + } + } + + return AbstractEagerCache::$caches[$key]; + } + + private function getCacheKey(): string + { + if (!isset($this->cacheName)) { + /* @see CacheHandler::getCacheName() */ + $reflection = new \ReflectionClass($this); + $this->cacheName = \str_replace( + ['\\', 'system_cache_eager_'], + ['_', ''], + \get_class($this) + ); + + $parameters = ClassUtil::getConstructorProperties($this); + + if ($parameters !== []) { + $this->cacheName .= '-' . CacheHandler::getInstance()->getCacheIndex($parameters); + } + } + + return $this->cacheName; + } + + /** + * Rebuilds the cache data and stores the updated data. + */ + final public function rebuild(): void + { + $key = $this->getCacheKey(); + $newCacheData = $this->getCacheData(); + + // The existing cache must not be overwritten, otherwise this can cause errors at runtime. + // The new data will be available at the next request. + if (!\array_key_exists($key, AbstractEagerCache::$caches)) { + AbstractEagerCache::$caches[$key] = $newCacheData; + } + + CacheHandler::getInstance()->getCacheSource()->set($key, $newCacheData, 0); + } + + /** + * Generates the cache data and returns it. + * This method MUST NOT rely on any (runtime) cache at any point because those could be stale. + * + * @return T + */ + abstract protected function getCacheData(): array|object; +} diff --git a/wcfsetup/install/files/lib/system/cache/eager/CoreObjectCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/CoreObjectCache.class.php new file mode 100644 index 00000000000..c75b56a63b9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/CoreObjectCache.class.php @@ -0,0 +1,36 @@ + + * @since 6.2 + * + * @extends AbstractEagerCache>> + */ +final class CoreObjectCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $coreObjectList = new CoreObjectList(); + $coreObjectList->readObjects(); + $coreObjects = $coreObjectList->getObjects(); + + $data = []; + foreach ($coreObjects as $coreObject) { + $tmp = \explode('\\', $coreObject->objectName); + $className = \array_pop($tmp); + $data[$className] = $coreObject->objectName; + } + + return $data; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/eager/LanguageCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/LanguageCache.class.php new file mode 100644 index 00000000000..ef0934af1b2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/LanguageCache.class.php @@ -0,0 +1,74 @@ + + * @since 6.2 + * + * @extends AbstractEagerCache + */ +final class LanguageCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): LanguageCacheData + { + $languageList = new LanguageList(); + $languageList->getConditionBuilder()->add('language.isDisabled = ?', [0]); + $languageList->readObjects(); + + $languages = $languageList->getObjects(); + $default = 0; + $multilingualismEnabled = false; + $codes = []; + $countryCodes = []; + + foreach ($languageList->getObjects() as $language) { + if ($language->isDefault) { + $default = $language->languageID; + } + + if ($language->hasContent) { + $multilingualismEnabled = true; + } + + $codes[$language->languageCode] = $language->languageID; + $countryCodes[$language->languageID] = $language->countryCode; + } + + DatabaseObject::sort($languages, 'languageName'); + + $languageCategoryList = new LanguageCategoryList(); + $languageCategoryList->readObjects(); + + $categories = []; + $categoryIDs = []; + foreach ($languageCategoryList->getObjects() as $languageCategory) { + $categories[$languageCategory->languageCategory] = $languageCategory; + $categoryIDs[$languageCategory->languageCategoryID] = $languageCategory->languageCategory; + } + + if (!isset($languages[$default])) { + throw new \RuntimeException('No default language defined!'); + } + + return new LanguageCacheData( + $codes, + $countryCodes, + $languages, + $default, + $categories, + $categoryIDs, + $multilingualismEnabled + ); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/eager/data/LanguageCacheData.class.php b/wcfsetup/install/files/lib/system/cache/eager/data/LanguageCacheData.class.php new file mode 100644 index 00000000000..8ade4cc321f --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/data/LanguageCacheData.class.php @@ -0,0 +1,121 @@ + + * @since 6.2 + */ +final class LanguageCacheData +{ + public function __construct( + /** @var array */ + public readonly array $codes, + /** @var array */ + public readonly array $countryCodes, + /** @var array */ + public readonly array $languages, + public readonly int $default, + /** @var array */ + public readonly array $categories, + /** @var array */ + public readonly array $categoryIDs, + public readonly bool $multilingualismEnabled + ) { + } + + /** + * Returns the default language. + */ + public function getDefaultLanguage(): Language + { + return $this->languages[$this->default]; + } + + /** + * Returns the language with the given language id. + */ + public function getLanguage(int $languageID): ?Language + { + return $this->languages[$languageID] ?? null; + } + + /** + * Returns the language category with the given category name. + */ + public function getCategory(string $categoryName): ?LanguageCategory + { + return $this->categories[$categoryName] ?? null; + } + + /** + * Returns the language category with the given category id. + */ + public function getCategoryByID(int $languageCategoryID): ?LanguageCategory + { + $categoryName = $this->categoryIDs[$languageCategoryID] ?? null; + if ($categoryName === null) { + return null; + } + + return $this->getCategory($categoryName); + } + + public function hasCategory(string $categoryName): bool + { + return \array_key_exists($categoryName, $this->categories); + } + + /** + * Return all content languages. + * + * @return array + */ + public function getContentLanguages(): array + { + return \array_filter( + $this->languages, + static fn(Language $language) => \boolval($language->hasContent) + ); + } + + /** + * Return all content languages IDs. + * + * @return list + */ + public function getContentLanguageIDs(): array + { + return \array_keys($this->getContentLanguages()); + } + + /** + * Return all language codes. + * + * @return list + */ + public function getLanguageCodes(): array + { + return \array_keys($this->codes); + } + + /** + * Return language by given language code. + */ + public function getLanguageByCode(string $languageCode): ?Language + { + $languageID = $this->codes[$languageCode] ?? null; + if ($languageID === null) { + return null; + } + + return $this->getLanguage($languageID); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/source/DiskCacheSource.class.php b/wcfsetup/install/files/lib/system/cache/source/DiskCacheSource.class.php index 11e84a6d0ba..8fd2a3c444e 100644 --- a/wcfsetup/install/files/lib/system/cache/source/DiskCacheSource.class.php +++ b/wcfsetup/install/files/lib/system/cache/source/DiskCacheSource.class.php @@ -157,4 +157,20 @@ private function readCache(string $filename): mixed throw new \Exception("Failed to unserialize the cache contents.", 0, $e); } } + + #[\Override] + public function getCreationTime(string $cacheName, int $maxLifetime): ?int + { + $filename = $this->getFilename($cacheName); + + if (!\file_exists($filename)) { + return null; + } + + if (!@\filesize($filename)) { + return null; + } + + return \filemtime($filename); + } } diff --git a/wcfsetup/install/files/lib/system/cache/source/ICacheSource.class.php b/wcfsetup/install/files/lib/system/cache/source/ICacheSource.class.php index 52535eeb8c8..9261c4d34f3 100644 --- a/wcfsetup/install/files/lib/system/cache/source/ICacheSource.class.php +++ b/wcfsetup/install/files/lib/system/cache/source/ICacheSource.class.php @@ -45,4 +45,10 @@ public function get($cacheName, $maxLifetime); * @return void */ public function set($cacheName, $value, $maxLifetime); + + /** + * Returns the timestamp when the cache was created. + * Or `null` if the cache does not exist or is empty. + */ + public function getCreationTime(string $cacheName, int $maxLifetime): ?int; } diff --git a/wcfsetup/install/files/lib/system/cache/source/RedisCacheSource.class.php b/wcfsetup/install/files/lib/system/cache/source/RedisCacheSource.class.php index c33f966f1f1..087a8d12e14 100644 --- a/wcfsetup/install/files/lib/system/cache/source/RedisCacheSource.class.php +++ b/wcfsetup/install/files/lib/system/cache/source/RedisCacheSource.class.php @@ -184,4 +184,28 @@ public function getRedis() { return $this->redis; } + + #[\Override] + public function getCreationTime(string $cacheName, int $maxLifetime): ?int + { + $parts = \explode('-', $cacheName, 2); + + if (isset($parts[1])) { + $ttl = $this->redis->ttl($this->getCacheName($parts[0])); + } else { + $ttl = $this->redis->ttl($this->getCacheName($cacheName)); + } + + // -2 means that the key does not exist + if ($ttl === -2) { + return null; + } + + // -1 means that the key exists but does not have an expiration date. + if ($ttl === -1) { + return \TIME_NOW; + } + + return $ttl - $maxLifetime; + } } diff --git a/wcfsetup/install/files/lib/system/cache/tolerant/AbstractTolerantCache.class.php b/wcfsetup/install/files/lib/system/cache/tolerant/AbstractTolerantCache.class.php new file mode 100644 index 00000000000..eaaf88ba428 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/tolerant/AbstractTolerantCache.class.php @@ -0,0 +1,130 @@ += 300`. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + * + * @template T of array|object + */ +abstract class AbstractTolerantCache +{ + /** + * @var T + */ + private array|object $cache; + private string $cacheName; + + /** + * @return T + */ + final public function getCache(): array|object + { + if (!isset($this->cache)) { + $cache = CacheHandler::getInstance()->getCacheSource()->get( + $this->getCacheKey(), + 0, + ); + + if ($cache === null) { + $this->rebuild(); + } else { + $this->cache = $cache; + } + + if ($this->needsRebuild()) { + BackgroundQueueHandler::getInstance()->enqueueIn([ + new TolerantCacheRebuildBackgroundJob( + \get_class($this), + ClassUtil::getConstructorProperties($this) + ) + ]); + BackgroundQueueHandler::getInstance()->forceCheck(); + } + } + return $this->cache; + } + + private function getCacheKey(): string + { + if (!isset($this->cacheName)) { + /* @see AbstractEagerCache::getCacheKey() */ + $this->cacheName = \str_replace( + ['\\', 'system_cache_tolerant_'], + ['_', ''], + \get_class($this) + ); + + $parameters = ClassUtil::getConstructorProperties($this); + + if ($parameters !== []) { + $this->cacheName .= '-' . CacheHandler::getInstance()->getCacheIndex($parameters); + } + } + + return $this->cacheName; + } + + final public function rebuild(): void + { + $newCacheData = $this->rebuildCacheData(); + + if (!isset($this->cache)) { + $this->cache = $newCacheData; + } + + CacheHandler::getInstance()->getCacheSource()->set( + $this->getCacheKey(), + $newCacheData, + 0 + ); + } + + /** + * @return T + */ + abstract protected function rebuildCacheData(): array|object; + + final public function nextRebuildTime(): int + { + $lifetime = $this->getLifetime(); + \assert($lifetime >= 300); + + $cacheTime = CacheHandler::getInstance()->getCacheSource()->getCreationTime( + $this->getCacheKey(), + $lifetime + ); + + if ($cacheTime === null) { + return \TIME_NOW; + } + + return $cacheTime + $lifetime; + } + + /** + * Return the lifetime of the cache in seconds. + */ + abstract public function getLifetime(): int; + + private function needsRebuild(): bool + { + // Probabilistic early expiration + // https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration + + return TIME_NOW - 10 * \log(\random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX) + >= $this->nextRebuildTime(); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/tolerant/UserBirthdayCache.class.php b/wcfsetup/install/files/lib/system/cache/tolerant/UserBirthdayCache.class.php new file mode 100644 index 00000000000..9c83ed89139 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/tolerant/UserBirthdayCache.class.php @@ -0,0 +1,54 @@ + + * @since 6.2 + * + * @extends AbstractTolerantCache>> + */ +final class UserBirthdayCache extends AbstractTolerantCache +{ + public function __construct(public readonly int $month) + { + } + + #[\Override] + public function getLifetime(): int + { + return 3600; + } + + #[\Override] + protected function rebuildCacheData(): array + { + $userOptionID = User::getUserOptionID('birthday'); + if ($userOptionID === null) { + // birthday profile field missing; skip + return []; + } + + $data = []; + $birthday = 'userOption' . $userOptionID; + $sql = "SELECT userID, " . $birthday . " + FROM wcf1_user_option_value + WHERE " . $birthday . " LIKE ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(['%-' . ($this->month < 10 ? '0' : '') . $this->month . '-%']); + while ($row = $statement->fetchArray()) { + [, , $day] = \explode('-', $row[$birthday]); + if (!isset($data[$day])) { + $data[$day] = []; + } + $data[\intval($day)][] = $row['userID']; + } + + return $data; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/tolerant/WhoWasOnlineCache.class.php b/wcfsetup/install/files/lib/system/cache/tolerant/WhoWasOnlineCache.class.php new file mode 100644 index 00000000000..92b641f182c --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/tolerant/WhoWasOnlineCache.class.php @@ -0,0 +1,41 @@ + + * @since 6.2 + * + * @extends AbstractTolerantCache> + */ +final class WhoWasOnlineCache extends AbstractTolerantCache +{ + #[\Override] + public function getLifetime(): int + { + return 600; + } + + #[\Override] + protected function rebuildCacheData(): array + { + $sql = "( + SELECT userID + FROM wcf1_user + WHERE lastActivityTime > ? + ) UNION ( + SELECT userID + FROM wcf1_session + WHERE userID IS NOT NULL + AND lastActivityTime > ? + )"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([TIME_NOW - 86400, TIME_NOW - USER_ONLINE_TIMEOUT]); + + return $statement->fetchAll(\PDO::FETCH_COLUMN); + } +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/languages/SetAsDefaultLanguage.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/languages/SetAsDefaultLanguage.class.php index 79b6c1fca86..f0e583ea3a1 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/languages/SetAsDefaultLanguage.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/languages/SetAsDefaultLanguage.class.php @@ -34,10 +34,6 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res $languageEditor = new LanguageEditor($language); $languageEditor->setAsDefault(); - if ($languageEditor->isDisabled) { - $languageEditor->update(['isDisabled' => 0]); - } - return new JsonResponse([]); } diff --git a/wcfsetup/install/files/lib/system/language/LanguageFactory.class.php b/wcfsetup/install/files/lib/system/language/LanguageFactory.class.php index 96c9d46bbf5..5ecd2932e0d 100644 --- a/wcfsetup/install/files/lib/system/language/LanguageFactory.class.php +++ b/wcfsetup/install/files/lib/system/language/LanguageFactory.class.php @@ -7,7 +7,8 @@ use wcf\data\language\category\LanguageCategory; use wcf\data\language\Language; use wcf\data\language\LanguageEditor; -use wcf\system\cache\builder\LanguageCacheBuilder; +use wcf\system\cache\eager\data\LanguageCacheData; +use wcf\system\cache\eager\LanguageCache; use wcf\system\SingletonFactory; use wcf\system\template\TemplateScriptingCompiler; use wcf\system\WCF; @@ -15,24 +16,16 @@ /** * Handles language related functions. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License - * @phpstan-import-type LanguageCache from LanguageCacheBuilder */ class LanguageFactory extends SingletonFactory { /** * language cache - * @var LanguageCache */ - protected $cache; - - /** - * initialized languages - * @var Language[] - */ - protected $languages = []; + protected LanguageCacheData $cache; /** * active template scripting compiler @@ -50,33 +43,19 @@ protected function init() /** * Returns a Language object for the language with the given id. - * - * @param int $languageID - * @return ?Language */ - public function getLanguage($languageID) + public function getLanguage(int $languageID): ?Language { - if (!isset($this->languages[$languageID])) { - if (!isset($this->cache['languages'][$languageID])) { - return null; - } - - $this->languages[$languageID] = $this->cache['languages'][$languageID]; - } - - return $this->languages[$languageID]; + return $this->cache->getLanguage($languageID); } /** * Returns the preferred language of the current user. - * - * @param int $languageID - * @return ?Language */ - public function getUserLanguage($languageID = 0) + public function getUserLanguage(?int $languageID = null): Language { if ($languageID) { - $language = $this->getLanguage($languageID); + $language = $this->cache->getLanguage($languageID); if ($language !== null) { return $language; } @@ -84,71 +63,50 @@ public function getUserLanguage($languageID = 0) $languageID = $this->findPreferredLanguage(); - return $this->getLanguage($languageID); + return $this->cache->getLanguage($languageID); } /** * Returns the language with the given language code or null if no such * language exists. - * - * @param string $languageCode - * @return ?Language */ - public function getLanguageByCode($languageCode) + public function getLanguageByCode(string $languageCode): ?Language { - // called within WCFSetup - if (empty($this->cache['codes'])) { - $sql = "SELECT languageID + if ($this->cache->codes === []) { + // called within WCFSetup + $sql = "SELECT * FROM wcf1_language WHERE languageCode = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([$languageCode]); - $row = $statement->fetchArray(); - if (isset($row['languageID'])) { - return new Language($row['languageID']); - } - } elseif (isset($this->cache['codes'][$languageCode])) { - return $this->getLanguage($this->cache['codes'][$languageCode]); + return $statement->fetchObject(Language::class); + } else { + return $this->getLanguageByCode($languageCode); } - - return null; } /** * Returns true if the language category with the given name exists. - * - * @param string $categoryName - * @return bool */ - public function isValidCategory($categoryName) + public function isValidCategory(string $categoryName): bool { - return isset($this->cache['categories'][$categoryName]); + return $this->cache->hasCategory($categoryName); } /** * Returns the language category with the given name. - * - * @param string $categoryName - * @return ?LanguageCategory */ - public function getCategory($categoryName) + public function getCategory(string $categoryName): ?LanguageCategory { - return $this->cache['categories'][$categoryName] ?? null; + return $this->cache->getCategory($categoryName); } /** * Returns language category by id. - * - * @param int $languageCategoryID - * @return ?LanguageCategory */ - public function getCategoryByID($languageCategoryID) + public function getCategoryByID(int $languageCategoryID): ?LanguageCategory { - if (isset($this->cache['categoryIDs'][$languageCategoryID])) { - return $this->cache['categories'][$this->cache['categoryIDs'][$languageCategoryID]]; - } - - return null; + return $this->cache->getCategoryByID($languageCategoryID); } /** @@ -156,9 +114,9 @@ public function getCategoryByID($languageCategoryID) * * @return LanguageCategory[] */ - public function getCategories() + public function getCategories(): array { - return $this->cache['categories']; + return $this->cache->categories; } /** @@ -166,19 +124,15 @@ public function getCategories() * * @return int */ - protected function findPreferredLanguage() + protected function findPreferredLanguage(): int { - // get available language codes - $availableLanguageCodes = []; - foreach ($this->getLanguages() as $language) { - $availableLanguageCodes[] = $language->languageCode; - } + $defaultLanguageCode = $this->cache->getDefaultLanguage()->languageCode; - $defaultLanguageCode = $this->cache['languages'][$this->cache['default']]->languageCode; - $languageCode = self::getPreferredLanguage($availableLanguageCodes, $defaultLanguageCode); + // get preferred language + $languageCode = self::getPreferredLanguage($this->cache->getLanguageCodes(), $defaultLanguageCode); // get language id of preferred language - foreach ($this->cache['languages'] as $key => $language) { + foreach ($this->cache->languages as $key => $language) { if ($language->languageCode === $languageCode) { return $key; } @@ -218,10 +172,8 @@ public static function getPreferredLanguage(array $availableLanguageCodes, strin /** * Returns the active scripting compiler object. - * - * @return TemplateScriptingCompiler */ - public function getScriptingCompiler() + public function getScriptingCompiler(): TemplateScriptingCompiler { if ($this->scriptingCompiler === null) { $this->scriptingCompiler = new TemplateScriptingCompiler(WCF::getTPL()); @@ -235,9 +187,9 @@ public function getScriptingCompiler() * * @return void */ - protected function loadCache() + protected function loadCache(): void { - $this->cache = LanguageCacheBuilder::getInstance()->getData(); + $this->cache = (new LanguageCache())->getCache(); } /** @@ -245,9 +197,9 @@ protected function loadCache() * * @return void */ - public function clearCache() + public function clearCache(): void { - LanguageCacheBuilder::getInstance()->reset(); + (new LanguageCache())->rebuild(); } /** @@ -264,23 +216,19 @@ public static function fixLanguageCode($languageCode) /** * Returns the default language object. - * - * @return Language * @since 3.0 */ - public function getDefaultLanguage() + public function getDefaultLanguage(): Language { - return $this->getLanguage($this->cache['default']); + return $this->cache->getDefaultLanguage(); } /** * Returns the default language id - * - * @return int */ - public function getDefaultLanguageID() + public function getDefaultLanguageID(): int { - return $this->cache['default']; + return $this->cache->default; } /** @@ -288,9 +236,9 @@ public function getDefaultLanguageID() * * @return Language[] */ - public function getLanguages() + public function getLanguages(): array { - return $this->cache['languages']; + return $this->cache->languages; } /** @@ -298,16 +246,9 @@ public function getLanguages() * * @return array */ - public function getContentLanguages() + public function getContentLanguages(): array { - $availableLanguages = []; - foreach ($this->getLanguages() as $languageID => $language) { - if ($language->hasContent) { - $availableLanguages[$languageID] = $language; - } - } - - return $availableLanguages; + return $this->cache->getContentLanguages(); } /** @@ -316,25 +257,15 @@ public function getContentLanguages() * @return int[] * @since 3.1 */ - public function getContentLanguageIDs() + public function getContentLanguageIDs(): array { - $languageIDs = []; - foreach ($this->getLanguages() as $language) { - if ($language->hasContent) { - $languageIDs[] = $language->languageID; - } - } - - return $languageIDs; + return $this->cache->getContentLanguageIDs(); } /** * Makes given language the default language. - * - * @param int $languageID - * @return void */ - public function makeDefault($languageID) + public function makeDefault(int $languageID): void { // remove old default language $sql = "UPDATE wcf1_language @@ -345,7 +276,8 @@ public function makeDefault($languageID) // make this language to default $sql = "UPDATE wcf1_language - SET isDefault = 1 + SET isDefault = 1, + isDisabled = 0 WHERE languageID = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([$languageID]); @@ -359,11 +291,11 @@ public function makeDefault($languageID) * * @return void */ - public function deleteLanguageCache() + public function deleteLanguageCache(): void { LanguageEditor::deleteLanguageFiles(); - foreach ($this->cache['languages'] as $language) { + foreach ($this->cache->languages as $language) { $languageEditor = new LanguageEditor($language); $languageEditor->deleteCompiledTemplates(); } @@ -371,20 +303,16 @@ public function deleteLanguageCache() /** * Returns true if multilingualism is enabled. - * - * @return bool */ - public function multilingualismEnabled() + public function multilingualismEnabled(): bool { - return $this->cache['multilingualismEnabled']; + return $this->cache->multilingualismEnabled; } /** * Returns the number of phrases that have been automatically disabled in the past 7 days. - * - * @return int */ - public function countRecentlyDisabledCustomValues() + public function countRecentlyDisabledCustomValues(): int { $sql = "SELECT COUNT(*) AS count FROM wcf1_language_item diff --git a/wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php index a3565380de3..8b20d71f9d9 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/CoreObjectPackageInstallationPlugin.class.php @@ -4,7 +4,7 @@ use wcf\data\core\object\CoreObjectEditor; use wcf\data\core\object\CoreObjectList; -use wcf\system\cache\builder\CoreObjectCacheBuilder; +use wcf\system\cache\eager\CoreObjectCache; use wcf\system\devtools\pip\IDevtoolsPipEntryList; use wcf\system\devtools\pip\IGuiPackageInstallationPlugin; use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin; @@ -85,7 +85,7 @@ protected function findExistingItem(array $data) */ protected function cleanup() { - CoreObjectCacheBuilder::getInstance()->reset(); + (new CoreObjectCache())->rebuild(); } /** diff --git a/wcfsetup/install/files/lib/system/user/UserBirthdayCache.class.php b/wcfsetup/install/files/lib/system/user/UserBirthdayCache.class.php index 65cfc490a78..098bf69b814 100644 --- a/wcfsetup/install/files/lib/system/user/UserBirthdayCache.class.php +++ b/wcfsetup/install/files/lib/system/user/UserBirthdayCache.class.php @@ -2,7 +2,6 @@ namespace wcf\system\user; -use wcf\system\cache\builder\UserBirthdayCacheBuilder; use wcf\system\event\EventHandler; use wcf\system\SingletonFactory; @@ -15,17 +14,12 @@ */ class UserBirthdayCache extends SingletonFactory { - /** - * loaded months - * @var int[] - */ - protected $monthsLoaded = []; - /** * user birthdays - * @var int[][] + * + * @var array>> */ - protected $birthdays = []; + protected array $birthdays = []; /** * Loads the birthday cache. @@ -33,14 +27,10 @@ class UserBirthdayCache extends SingletonFactory * @param int $month * @return void */ - protected function loadMonth($month) + protected function loadMonth(int $month): void { - if (!isset($this->monthsLoaded[$month])) { - $this->birthdays = \array_merge( - $this->birthdays, - UserBirthdayCacheBuilder::getInstance()->getData(['month' => $month]) - ); - $this->monthsLoaded[$month] = true; + if (!\array_key_exists($month, $this->birthdays)) { + $this->birthdays[$month] = (new \wcf\system\cache\tolerant\UserBirthdayCache($month))->getCache(); $data = [ 'birthdays' => $this->birthdays, @@ -53,20 +43,12 @@ protected function loadMonth($month) /** * Returns the user birthdays for a specific day. - * - * @param int $month - * @param int $day * @return int[] list of user ids */ - public function getBirthdays($month, $day) + public function getBirthdays(int $month, int $day): array { $this->loadMonth($month); - $index = ($month < 10 ? '0' : '') . $month . '-' . ($day < 10 ? '0' : '') . $day; - if (isset($this->birthdays[$index])) { - return $this->birthdays[$index]; - } - - return []; + return $this->birthdays[$month][$day] ?? []; } } diff --git a/wcfsetup/install/files/lib/util/ClassUtil.class.php b/wcfsetup/install/files/lib/util/ClassUtil.class.php index 3e317b88953..022a0ce695e 100644 --- a/wcfsetup/install/files/lib/util/ClassUtil.class.php +++ b/wcfsetup/install/files/lib/util/ClassUtil.class.php @@ -97,6 +97,29 @@ public static function isDecoratedInstanceOf($className, $targetClass) return false; } + /** + * Returns the properties as a key-value array to construct the given object. + * + * @return array + */ + public static function getConstructorProperties(object $object): array + { + $reflection = new \ReflectionClass($object); + + $properties = []; + foreach ($reflection->getConstructor()?->getParameters() ?? [] as $parameter) { + $property = $reflection->getProperty($parameter->getName()); + + if (!$property->isInitialized($object)) { + continue; + } + + $properties[$property->getName()] = $property->getValue($object); + } + + return $properties; + } + /** * Forbid creation of ClassUtil objects. */