diff --git a/composer.json b/composer.json index 8b4e2cab21e..f35cb058ac9 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,8 @@ "colinmollenhour/credis": "1.17.0", "composer/package-versions-deprecated": "1.11.99.5", "composer/semver": "3.4.3", + "doctrine/doctrine-orm-module": "^6.0", + "doctrine/orm": "^2.10.2", "endroid/qr-code": "5.1.0", "ezyang/htmlpurifier": "4.17.0", "firebase/php-jwt": "6.11.1", @@ -64,7 +66,6 @@ "laminas/laminas-cache-storage-adapter-memory": "^2.0", "laminas/laminas-captcha": "2.18.0", "laminas/laminas-code": "4.16.0", - "laminas/laminas-db": "2.20.0", "laminas/laminas-diactoros": "3.5.0", "laminas/laminas-escaper": "2.14.0", "laminas/laminas-eventmanager": "3.14.0", @@ -78,7 +79,6 @@ "laminas/laminas-mvc-i18n": "1.9.0", "laminas/laminas-mvc-plugin-flashmessenger": "1.11.0", "laminas/laminas-paginator": "2.19.0", - "laminas/laminas-paginator-adapter-laminasdb": "1.4.1", "laminas/laminas-psr7bridge": "1.12.0", "laminas/laminas-recaptcha": "3.8.0", "laminas/laminas-serializer": "2.18.0", diff --git a/composer.lock b/composer.lock index 9d754f89cce..54af105389e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f56858457053cff5b05c203eb55ad050", + "content-hash": "b94999d61049a2ac6d75ccaff3cf4626", "packages": [ { "name": "ahand/mobileesp", @@ -811,6 +811,787 @@ }, "time": "2024-07-08T12:26:09+00:00" }, + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "time": "2024-09-05T10:17:24+00:00" + }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/collections", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/2eb07e5953eed811ce1b309a7478a3b236f2273d", + "reference": "2eb07e5953eed811ce1b309a7478a3b236f2273d", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1", + "symfony/polyfill-php84": "^1.30" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Collections\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.", + "homepage": "https://www.doctrine-project.org/projects/collections.html", + "keywords": [ + "array", + "collections", + "iterators", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/collections/issues", + "source": "https://github.com/doctrine/collections/tree/2.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcollections", + "type": "tidelift" + } + ], + "time": "2025-03-22T10:17:19+00:00" + }, + { + "name": "doctrine/common", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], + "time": "2025-01-01T22:12:03+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/1cf840d696373ea0d58ad0a8875c0fadcfc67214", + "reference": "1cf840d696373ea0d58ad0a8875c0fadcfc67214", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-07-10T21:11:04+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/doctrine-laminas-hydrator", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/doctrine-laminas-hydrator.git", + "reference": "aff2fbeaf214889b53626df5ee1d68eb283cc50f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/doctrine-laminas-hydrator/zipball/aff2fbeaf214889b53626df5ee1d68eb283cc50f", + "reference": "aff2fbeaf214889b53626df5ee1d68eb283cc50f", + "shasum": "" + }, + "require": { + "doctrine/collections": "^2.0.0", + "doctrine/inflector": "^2.0.4", + "doctrine/persistence": "^3.4.0 || ^4.0.0", + "ext-ctype": "*", + "laminas/laminas-hydrator": "^4.13.0", + "laminas/laminas-stdlib": "^3.14.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "phpdocumentor/guides-cli": "^1.5.0", + "phpstan/phpstan": "^2.0.2", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpunit/phpunit": "^10.5.38" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Laminas\\Hydrator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Doctrine hydrators for Laminas applications", + "keywords": [ + "doctrine", + "hydrator", + "laminas" + ], + "support": { + "issues": "https://github.com/doctrine/doctrine-laminas-hydrator/issues", + "rss": "https://github.com/doctrine/doctrine-laminas-hydrator/releases.atom", + "source": "https://github.com/doctrine/doctrine-laminas-hydrator" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-laminas-hydrator", + "type": "tidelift" + } + ], + "time": "2025-01-03T16:56:17+00:00" + }, + { + "name": "doctrine/doctrine-module", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineModule.git", + "reference": "bac2ff88f29edaa2b25e17128214f8661a6024aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineModule/zipball/bac2ff88f29edaa2b25e17128214f8661a6024aa", + "reference": "bac2ff88f29edaa2b25e17128214f8661a6024aa", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "composer/semver": "^3.0.0", + "doctrine/annotations": "^2.0.0", + "doctrine/cache": "^2.1.0", + "doctrine/collections": "^2.0.0", + "doctrine/doctrine-laminas-hydrator": "^3.2.0", + "doctrine/event-manager": "^2.0.0", + "doctrine/inflector": "^2.0.6", + "doctrine/persistence": "^3.0.0", + "laminas/laminas-authentication": "^2.12.0", + "laminas/laminas-cache": "^3.6.0", + "laminas/laminas-cache-storage-adapter-filesystem": "^2.2.0", + "laminas/laminas-cache-storage-adapter-memory": "^2.1.0", + "laminas/laminas-eventmanager": "^3.5.0", + "laminas/laminas-form": "^3.4.1", + "laminas/laminas-mvc": "^3.3.5", + "laminas/laminas-paginator": "^2.13.0", + "laminas/laminas-servicemanager": "^3.17.0", + "laminas/laminas-stdlib": "^3.13.0", + "laminas/laminas-validator": "^2.25.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/container": "^1.1.2", + "symfony/console": "^6.2.1 || ^7.0.0" + }, + "conflict": { + "doctrine/doctrine-mongo-odm-module": "<5.3.0", + "doctrine/doctrine-orm-module": "<6.3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "doctrine/mongodb-odm": "^2.7.1", + "doctrine/orm": "^2.20.1", + "jangregor/phpstan-prophecy": "^2.0.0", + "laminas/laminas-modulemanager": "^2.12.0", + "laminas/laminas-session": "^2.22.1", + "phpdocumentor/guides-cli": "^1.5.0", + "phpstan/phpstan": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.3", + "phpunit/phpunit": "^10.5.40" + }, + "suggest": { + "doctrine/data-fixtures": "Data Fixtures if you want to generate test data or bootstrap data for your deployments", + "doctrine/doctrine-mongo-odm-module": "For use with Doctrine MongDB ODM", + "doctrine/doctrine-orm-module": "For use with Doctrine ORM" + }, + "bin": [ + "bin/doctrine-module" + ], + "type": "library", + "extra": { + "laminas": { + "module": "DoctrineModule", + "config-provider": "DoctrineModule\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "DoctrineModule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laminas Module that provides Doctrine basic functionality required for ORM and ODM modules", + "homepage": "https://www.doctrine-project.org/", + "keywords": [ + "doctrine", + "laminas", + "module" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineModule/issues", + "source": "https://github.com/doctrine/DoctrineModule/tree/6.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-module", + "type": "tidelift" + } + ], + "time": "2025-01-23T18:55:44+00:00" + }, + { + "name": "doctrine/doctrine-orm-module", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineORMModule.git", + "reference": "189162f1839674785fcf97a4a6f41b48fd8f4409" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineORMModule/zipball/189162f1839674785fcf97a4a6f41b48fd8f4409", + "reference": "189162f1839674785fcf97a4a6f41b48fd8f4409", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^3.3.2", + "doctrine/doctrine-laminas-hydrator": "^3.2.0", + "doctrine/doctrine-module": "^6.3.0", + "doctrine/event-manager": "^2.0.0", + "doctrine/orm": "^2.13.0", + "doctrine/persistence": "^3.0.0", + "ext-json": "*", + "laminas/laminas-eventmanager": "^3.5.0", + "laminas/laminas-modulemanager": "^2.11.0", + "laminas/laminas-mvc": "^3.3.5", + "laminas/laminas-paginator": "^2.13.0", + "laminas/laminas-servicemanager": "^3.17.0", + "laminas/laminas-stdlib": "^3.13.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/container": "^1.1.2", + "symfony/console": "^6.1.2 || ^7.0.0" + }, + "conflict": { + "doctrine/data-fixtures": "<2.0", + "doctrine/migrations": "<3.8", + "laminas/laminas-form": "<3.10" + }, + "require-dev": { + "doctrine/annotations": "^2.0.0", + "doctrine/coding-standard": "^12.0.0", + "doctrine/data-fixtures": "^2.0.1", + "doctrine/migrations": "^3.8.0", + "laminas/laminas-cache-storage-adapter-filesystem": "^2.0", + "laminas/laminas-cache-storage-adapter-memory": "^2.0", + "laminas/laminas-developer-tools": "^2.3.0", + "laminas/laminas-i18n": "^2.23.0", + "laminas/laminas-serializer": "^2.12.0", + "phpstan/phpstan": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.3", + "phpunit/phpunit": "^10.5.40" + }, + "suggest": { + "doctrine/migrations": "doctrine migrations if you want to keep your schema definitions versioned", + "laminas/laminas-developer-tools": "laminas-developer-tools if you want to profile operations executed by the ORM during development", + "laminas/laminas-form": "if you want to use form elements backed by Doctrine" + }, + "type": "library", + "extra": { + "laminas": { + "module": "DoctrineORMModule", + "config-provider": "DoctrineORMModule\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "DoctrineORMModule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laminas Module that provides Doctrine ORM functionality", + "homepage": "https://www.doctrine-project.org/", + "keywords": [ + "doctrine", + "laminas", + "module", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineORMModule/issues", + "source": "https://github.com/doctrine/DoctrineORMModule/tree/6.3.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-orm-module", + "type": "tidelift" + } + ], + "time": "2025-01-28T08:28:57+00:00" + }, { "name": "doctrine/event-manager", "version": "2.0.1", @@ -902,6 +1683,167 @@ ], "time": "2024-05-22T20:47:39+00:00" }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -929,7 +1871,118 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "src" + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "doctrine/orm", + "version": "2.20.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "6307b4fa7d7e3845a756106977e3b48907622098" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/6307b4fa7d7e3845a756106977e3b48907622098", + "reference": "6307b4fa7d7e3845a756106977e3b48907622098", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.12.1 || ^2.1.1", + "doctrine/collections": "^1.5 || ^2.1", + "doctrine/common": "^3.0.3", + "doctrine/dbal": "^2.13.1 || ^3.2", + "doctrine/deprecations": "^0.5.3 || ^1", + "doctrine/event-manager": "^1.2 || ^2", + "doctrine/inflector": "^1.4 || ^2.0", + "doctrine/instantiator": "^1.3 || ^2", + "doctrine/lexer": "^2 || ^3", + "doctrine/persistence": "^2.4 || ^3", + "ext-ctype": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3", + "symfony/console": "^4.2 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php72": "^1.23", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "doctrine/annotations": "<1.13 || >= 3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.13 || ^2", + "doctrine/coding-standard": "^9.0.2 || ^13.0", + "phpbench/phpbench": "^0.16.10 || ^1.0", + "phpstan/extension-installer": "~1.1.0 || ^1.4", + "phpstan/phpstan": "~1.4.10 || 2.0.3", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.12.0", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-dom": "Provides support for XSD validation for XML mapping files", + "symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0", + "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" + }, + "bin": [ + "bin/doctrine" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -946,38 +1999,29 @@ "email": "roman@code-factory.org" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" } ], - "time": "2024-02-05T11:56:58+00:00" + "description": "Object-Relational-Mapper for PHP", + "homepage": "https://www.doctrine-project.org/projects/orm.html", + "keywords": [ + "database", + "orm" + ], + "support": { + "issues": "https://github.com/doctrine/orm/issues", + "source": "https://github.com/doctrine/orm/tree/2.20.5" + }, + "time": "2025-06-24T17:50:46+00:00" }, { "name": "doctrine/persistence", @@ -1277,16 +2321,16 @@ }, { "name": "filp/whoops", - "version": "2.18.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -1336,7 +2380,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.0" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -1344,7 +2388,7 @@ "type": "github" } ], - "time": "2025-03-15T12:00:00+00:00" + "time": "2025-06-16T00:02:10+00:00" }, { "name": "firebase/php-jwt", @@ -1736,16 +2780,16 @@ }, { "name": "jaybizzle/crawler-detect", - "version": "v1.3.4", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/JayBizzle/Crawler-Detect.git", - "reference": "d3b7ff28994e1b0de764ab7412fa269a79634ff3" + "reference": "fbf1a3e81d61b088e7af723fb3c7a4ee92ac7e34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/d3b7ff28994e1b0de764ab7412fa269a79634ff3", - "reference": "d3b7ff28994e1b0de764ab7412fa269a79634ff3", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/fbf1a3e81d61b088e7af723fb3c7a4ee92ac7e34", + "reference": "fbf1a3e81d61b088e7af723fb3c7a4ee92ac7e34", "shasum": "" }, "require": { @@ -1782,9 +2826,84 @@ ], "support": { "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", - "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.3.4" + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.3.5" + }, + "time": "2025-06-11T17:58:05+00:00" + }, + { + "name": "laminas/laminas-authentication", + "version": "2.18.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-authentication.git", + "reference": "c1da3ec75bd4d6e3c63cf3a89f0f1a59a81a82bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-authentication/zipball/c1da3ec75bd4d6e3c63cf3a89f0f1a59a81a82bd", + "reference": "c1da3ec75bd4d6e3c63cf3a89f0f1a59a81a82bd", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "laminas/laminas-stdlib": "^3.19.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "conflict": { + "zendframework/zend-authentication": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.5.0", + "laminas/laminas-db": "^2.20.0", + "laminas/laminas-http": "^2.19.0", + "laminas/laminas-ldap": "^2.18.1", + "laminas/laminas-session": "^2.21.0", + "laminas/laminas-uri": "^2.12.0", + "laminas/laminas-validator": "^2.64.1", + "phpunit/phpunit": "^9.6.20", + "psalm/plugin-phpunit": "^0.19.0", + "squizlabs/php_codesniffer": "^3.10.2", + "vimeo/psalm": "^5.26.0" + }, + "suggest": { + "laminas/laminas-db": "Laminas\\Db component", + "laminas/laminas-http": "Laminas\\Http component", + "laminas/laminas-ldap": "Laminas\\Ldap component", + "laminas/laminas-session": "Laminas\\Session component", + "laminas/laminas-uri": "Laminas\\Uri component", + "laminas/laminas-validator": "Laminas\\Validator component" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Authentication\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides an API for authentication and includes concrete authentication adapters for common use case scenarios", + "homepage": "https://laminas.dev", + "keywords": [ + "Authentication", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-authentication/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-authentication/issues", + "rss": "https://github.com/laminas/laminas-authentication/releases.atom", + "source": "https://github.com/laminas/laminas-authentication" }, - "time": "2025-03-05T23:12:10+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-10-21T10:45:35+00:00" }, { "name": "laminas/laminas-cache", @@ -2360,77 +3479,6 @@ "abandoned": true, "time": "2024-12-05T14:32:05+00:00" }, - { - "name": "laminas/laminas-db", - "version": "2.20.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-db.git", - "reference": "207b9ee70a8b518913c1fad688d7a64fe89a8b91" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-db/zipball/207b9ee70a8b518913c1fad688d7a64fe89a8b91", - "reference": "207b9ee70a8b518913c1fad688d7a64fe89a8b91", - "shasum": "" - }, - "require": { - "laminas/laminas-stdlib": "^3.7.1", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0" - }, - "conflict": { - "zendframework/zend-db": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "^2.4.0", - "laminas/laminas-eventmanager": "^3.6.0", - "laminas/laminas-hydrator": "^4.7", - "laminas/laminas-servicemanager": "^3.19.0", - "phpunit/phpunit": "^9.5.25" - }, - "suggest": { - "laminas/laminas-eventmanager": "Laminas\\EventManager component", - "laminas/laminas-hydrator": "(^3.2 || ^4.3) Laminas\\Hydrator component for using HydratingResultSets", - "laminas/laminas-servicemanager": "Laminas\\ServiceManager component" - }, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Db", - "config-provider": "Laminas\\Db\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\Db\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Database abstraction layer, SQL abstraction, result set abstraction, and RowDataGateway and TableDataGateway implementations", - "homepage": "https://laminas.dev", - "keywords": [ - "db", - "laminas" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-db/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-db/issues", - "rss": "https://github.com/laminas/laminas-db/releases.atom", - "source": "https://github.com/laminas/laminas-db" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2024-04-02T01:04:56+00:00" - }, { "name": "laminas/laminas-diactoros", "version": "3.5.0", @@ -3794,69 +4842,6 @@ ], "time": "2024-10-16T13:10:19+00:00" }, - { - "name": "laminas/laminas-paginator-adapter-laminasdb", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-paginator-adapter-laminasdb.git", - "reference": "7f60bbdbc0339703c0e8b67b5c20d49cdab30bb2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-paginator-adapter-laminasdb/zipball/7f60bbdbc0339703c0e8b67b5c20d49cdab30bb2", - "reference": "7f60bbdbc0339703c0e8b67b5c20d49cdab30bb2", - "shasum": "" - }, - "require": { - "laminas/laminas-db": "^2.13.4", - "laminas/laminas-paginator": "^2.12.1", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "phpunit/phpunit": "^9.5.26", - "psalm/plugin-phpunit": "^0.18.0", - "vimeo/psalm": "^5.21" - }, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Paginator\\Adapter\\LaminasDb", - "config-provider": "Laminas\\Paginator\\Adapter\\LaminasDb\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\Paginator\\Adapter\\LaminasDb\\": "src//" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "laminas-db adapters for laminas-paginator", - "keywords": [ - "db", - "laminas", - "pagination" - ], - "support": { - "docs": "https://docs.laminas.dev/laminas-laminas-paginator-adapter-db/", - "forum": "https://discourse.laminas.dev/", - "issues": "https://github.com/laminas/laminas-laminas-paginator-adapter-db/issues", - "rss": "https://github.com/laminas/laminas-laminas-paginator-adapter-db/releases.atom", - "source": "https://github.com/laminas/laminas-laminas-paginator-adapter-db" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "abandoned": true, - "time": "2024-12-05T16:59:59+00:00" - }, { "name": "laminas/laminas-psr7bridge", "version": "1.12.0", @@ -5094,16 +6079,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -5127,13 +6112,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -5171,22 +6156,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -5220,9 +6205,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/mime-type-detection", @@ -6149,16 +7134,16 @@ }, { "name": "nette/utils", - "version": "v4.0.6", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "ce708655043c7050eb050df361c5e313cf708309" + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309", - "reference": "ce708655043c7050eb050df361c5e313cf708309", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "shasum": "" }, "require": { @@ -6229,9 +7214,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.6" + "source": "https://github.com/nette/utils/tree/v4.0.7" }, - "time": "2025-03-30T21:06:30+00:00" + "time": "2025-06-03T04:55:08+00:00" }, { "name": "nikic/php-parser", @@ -7899,21 +8884,20 @@ }, { "name": "ramsey/uuid", - "version": "4.8.1", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", - "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -7972,9 +8956,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.8.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "time": "2025-06-01T06:28:46+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "sebastian/version", @@ -9401,6 +10385,82 @@ ], "time": "2025-01-02T08:10:11+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, { "name": "symfony/rate-limiter", "version": "v6.4.15", @@ -11119,16 +12179,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -11167,7 +12227,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -11175,7 +12235,7 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "pdepend/pdepend", @@ -13902,16 +14962,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.22", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9" + "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9", - "reference": "8cb11f833d1f5bfbb2df97dfc23c92b4d42c18d9", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0d9f24f3de0a83573fce5c9ed025d6306c6e166b", + "reference": "0d9f24f3de0a83573fce5c9ed025d6306c6e166b", "shasum": "" }, "require": { @@ -13963,7 +15023,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.22" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.23" }, "funding": [ { @@ -13979,7 +15039,7 @@ "type": "tidelift" } ], - "time": "2025-05-17T07:35:26+00:00" + "time": "2025-06-23T06:49:06+00:00" }, { "name": "symfony/finder", @@ -14382,7 +15442,7 @@ "platform": { "php": ">=8.1" }, - "platform-dev": {}, + "platform-dev": [], "platform-overrides": { "php": "8.1" }, diff --git a/config/application.config.php b/config/application.config.php index 266ec770059..30f17cb9eb4 100644 --- a/config/application.config.php +++ b/config/application.config.php @@ -4,6 +4,8 @@ // Set up modules: $modules = [ + 'DoctrineModule', + 'DoctrineORMModule', 'Laminas\Cache', 'Laminas\Cache\Storage\Adapter\BlackHole', 'Laminas\Cache\Storage\Adapter\Filesystem', diff --git a/config/cli-config.php b/config/cli-config.php new file mode 100644 index 00000000000..beea8182b82 --- /dev/null +++ b/config/cli-config.php @@ -0,0 +1,6 @@ +getServiceManager()->get('doctrine.entitymanager.orm_vufind') +); diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 573b45494a7..690c9e4bb44 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -830,6 +830,7 @@ database = mysql://root@localhost/vufind ;database_name = "vufind" ; Should SSL be enabled on connections? (Currently only supported for MySQL). +; Also requires appropriate extra_options settings below. ; IMPORTANT: when using Linux, if your database connection string above uses ; "localhost", MySQL will automatically use a Unix socket connection. To force ; an SSL connection, change "localhost" to the IP address (e.g. "127.0.0.1"). @@ -847,7 +848,7 @@ use_ssl = false ; Using SSL" section of the Connector/J manual for details. verify_server_certificate = false -; This setting can be used to send additional options to the Laminas DB adapter. +; This setting can be used to send additional options to the Doctrine DB connection. ; This is useful for advanced features unsupported by settings above, such as ; detailed SSL security configuration. ;extra_options[client_key] = "/path/to/key" @@ -859,9 +860,8 @@ verify_server_certificate = false ;schema = schema_name ; The character set of the database -- utf8 and utf8mb4 are currently the only -; supported values, and utf8mb4 is the default if no value is set here. If you have -; a legacy VuFind 1.x database encoded in latin1, please upgrade it to utf8 using -; VuFind 7.x or earlier. +; supported values, and utf8mb4 is the default if no value is set here. (Ignored +; for non-MySQL databases). ;charset = utf8 ; Reduce access to a set of single passwords diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index 09fdefd857c..2bcc961f08d 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -464,10 +464,11 @@ 'VuFind\Crypt\PasswordHasher' => 'Laminas\ServiceManager\Factory\InvokableFactory', 'VuFind\Crypt\SecretCalculator' => 'VuFind\Crypt\SecretCalculatorFactory', 'VuFind\Date\Converter' => 'VuFind\Service\DateConverterFactory', - 'VuFind\Db\AdapterFactory' => 'VuFind\Service\ServiceWithConfigIniFactory', - 'VuFind\Db\Row\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', + 'VuFind\Db\ConnectionFactory' => 'VuFind\Db\ConnectionFactoryFactory', + 'VuFind\Db\Connection' => 'VuFind\Db\ConnectionFactory', + 'VuFind\Db\Entity\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', + 'VuFind\Db\PersistenceManager' => 'VuFind\Db\PersistenceManagerFactory', 'VuFind\Db\Service\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', - 'VuFind\Db\Table\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', 'VuFind\DigitalContent\OverdriveConnector' => 'VuFind\DigitalContent\OverdriveConnectorFactory', 'VuFind\Escaper\Escaper' => 'VuFind\Escaper\EscaperFactory', 'VuFind\Export' => 'VuFind\ExportFactory', @@ -555,7 +556,6 @@ 'VuFind\Validator\SessionCsrf' => 'VuFind\Validator\SessionCsrfFactory', 'VuFindHttp\HttpService' => 'VuFind\Service\HttpServiceFactory', 'VuFindSearch\Service' => 'VuFind\Service\SearchServiceFactory', - 'Laminas\Db\Adapter\Adapter' => 'VuFind\Db\AdapterFactory', 'Laminas\Session\SessionManager' => 'VuFind\Session\ManagerFactory', ], 'delegators' => [ @@ -570,6 +570,8 @@ 'VuFind\ServiceManager\ServiceInitializer', ], 'aliases' => [ + 'doctrine.connection.orm_vufind' => 'VuFind\Db\Connection', + 'VuFind\AccountCapabilities' => 'VuFind\Config\AccountCapabilities', 'VuFind\AuthManager' => 'VuFind\Auth\Manager', 'VuFind\AuthPluginManager' => 'VuFind\Auth\PluginManager', @@ -586,10 +588,6 @@ 'VuFind\ContentTOCPluginManager' => 'VuFind\Content\TOC\PluginManager', 'VuFind\CookieManager' => 'VuFind\Cookie\CookieManager', 'VuFind\DateConverter' => 'VuFind\Date\Converter', - 'VuFind\DbAdapter' => 'Laminas\Db\Adapter\Adapter', - 'VuFind\DbAdapterFactory' => 'VuFind\Db\AdapterFactory', - 'VuFind\DbRowPluginManager' => 'VuFind\Db\Row\PluginManager', - 'VuFind\DbTablePluginManager' => 'VuFind\Db\Table\PluginManager', 'VuFind\HierarchicalFacetHelper' => 'VuFind\Search\Solr\HierarchicalFacetHelper', 'VuFind\HierarchyDriverPluginManager' => 'VuFind\Hierarchy\Driver\PluginManager', 'VuFind\HierarchyTreeDataFormatterPluginManager' => 'VuFind\Hierarchy\TreeDataFormatter\PluginManager', @@ -643,6 +641,47 @@ 'VuFind\Http\CachingDownloader' => false, ], ], + 'caches' => [ + 'doctrinemodule.cache.filesystem' => [ + 'options' => [ + 'cache_dir' => LOCAL_CACHE_DIR . (PHP_SAPI == 'cli' ? '/cli' : '') . '/objects', + ], + ], + ], + 'doctrine' => [ + 'configuration' => [ + 'orm_vufind' => [ + 'query_cache' => 'filesystem', + 'result_cache' => 'filesystem', + 'metadata_cache' => 'filesystem', + 'hydration_cache' => 'filesystem', + 'proxy_dir' => LOCAL_CACHE_DIR . (PHP_SAPI == 'cli' ? '/cli' : '') . '/doctrine-proxies', + ], + ], + 'driver' => [ + 'vufind_attribute_driver' => [ + 'class' => \Doctrine\ORM\Mapping\Driver\AttributeDriver::class, + 'cache' => 'filesystem', + 'paths' => [ + 'module/VuFind/src/VuFind/Db/Entity', + ], + ], + 'orm_default' => [ + 'drivers' => [ + 'VuFind\Db\Entity' => 'vufind_attribute_driver', + ], + ], + ], + 'entitymanager' => [ + 'orm_vufind' => [ + 'connection' => 'orm_vufind', + 'configuration' => 'orm_vufind', + ], + ], + ], + 'doctrine_factories' => [ + 'entitymanager' => \VuFind\Db\EntityManagerFactory::class, + ], 'translator' => [], 'translator_plugins' => [ 'factories' => [ @@ -669,27 +708,6 @@ 'vufind' => [ // The config reader is a special service manager for loading .ini files: 'config_reader' => [ /* see VuFind\Config\PluginManager for defaults */ ], - // PostgreSQL sequence mapping - 'pgsql_seq_mapping' => [ - 'auth_hash' => ['id', 'auth_hash_id_seq'], - 'comments' => ['id', 'comments_id_seq'], - 'external_session' => ['id', 'external_session_id_seq'], - 'feedback' => ['id', 'feedback_id_seq'], - 'oai_resumption' => ['id', 'oai_resumption_id_seq'], - 'ratings' => ['id', 'ratings_id_seq'], - 'record' => ['id', 'record_id_seq'], - 'resource' => ['id', 'resource_id_seq'], - 'resource_tags' => ['id', 'resource_tags_id_seq'], - 'search' => ['id', 'search_id_seq'], - 'session' => ['id', 'session_id_seq'], - 'shortlinks' => ['id', 'shortlinks_id_seq'], - 'tags' => ['id', 'tags_id_seq'], - 'user' => ['id', 'user_id_seq'], - 'user_card' => ['id', 'user_card_id_seq'], - 'user_list' => ['id', 'user_list_id_seq'], - 'user_resource' => ['id', 'user_resource_id_seq'], - 'login_token' => ['id', 'login_token_id_seq'], - ], // This section contains service manager configurations for all VuFind // pluggable components: 'plugin_managers' => [ @@ -708,9 +726,8 @@ 'content_toc' => [ /* see VuFind\Content\TOC\PluginManager for defaults */ ], 'contentblock' => [ /* see VuFind\ContentBlock\PluginManager for defaults */ ], 'cover_layer' => [ /* see VuFind\Cover\Layer\PluginManager for defaults */ ], - 'db_row' => [ /* see VuFind\Db\Row\PluginManager for defaults */ ], + 'db_entity' => [ /* see VuFind\Db\Entity\PluginManager for defaults */ ], 'db_service' => [ /* see VuFind\Db\Service\PluginManager for defaults */ ], - 'db_table' => [ /* see VuFind\Db\Table\PluginManager for defaults */ ], 'form_handler' => [ /* see VuFind\Form\Handler\PluginManager for defaults */], 'hierarchy_driver' => [ /* see VuFind\Hierarchy\Driver\PluginManager for defaults */ ], 'hierarchy_treedataformatter' => [ /* see VuFind\Hierarchy\TreeDataFormatter\PluginManager for defaults */ ], diff --git a/module/VuFind/src/VuFind/AjaxHandler/GetSearchResults.php b/module/VuFind/src/VuFind/AjaxHandler/GetSearchResults.php index 2f4c2d993ef..ab2ca4801e9 100644 --- a/module/VuFind/src/VuFind/AjaxHandler/GetSearchResults.php +++ b/module/VuFind/src/VuFind/AjaxHandler/GetSearchResults.php @@ -34,7 +34,6 @@ use Laminas\View\Model\ViewModel; use Laminas\View\Renderer\PhpRenderer; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\Search; use VuFind\Record\Loader as RecordLoader; use VuFind\Search\Base\Results; use VuFind\Search\Memory; diff --git a/module/VuFind/src/VuFind/Auth/Database.php b/module/VuFind/src/VuFind/Auth/Database.php index 8dd10f47e7a..3ee98aa7a35 100644 --- a/module/VuFind/src/VuFind/Auth/Database.php +++ b/module/VuFind/src/VuFind/Auth/Database.php @@ -38,6 +38,7 @@ use VuFind\Db\Service\UserServiceInterface; use VuFind\Exception\Auth as AuthException; use VuFind\Exception\AuthEmailNotVerified as AuthEmailNotVerifiedException; +use VuFind\Exception\DuplicateKeyException; use function in_array; use function is_object; @@ -145,19 +146,6 @@ protected function setUserPassword(UserEntityInterface $user, string $pass): voi } } - /** - * Does the provided exception indicate that a duplicate key value has been - * created? - * - * @param \Exception $e Exception to check - * - * @return bool - */ - protected function exceptionIndicatesDuplicateKey(\Exception $e): bool - { - return strstr($e->getMessage(), 'Duplicate entry') !== false; - } - /** * Create a new user account from the request. * @@ -185,7 +173,7 @@ public function create($request) $user = $this->createUserFromParams($params, $userService); try { $userService->persistEntity($user); - } catch (\Laminas\Db\Adapter\Exception\RuntimeException $e) { + } catch (DuplicateKeyException $e) { // In a scenario where the unique key of the user table is // shorter than the username field length, it is possible that // a user will pass validation but still get rejected due to @@ -193,8 +181,7 @@ public function create($request) // unlikely scenario, but if it occurs, we will treat it the // same as a duplicate username. Other unexpected database // errors will be passed through unmodified. - throw $this->exceptionIndicatesDuplicateKey($e) - ? new AuthException('That username is already taken') : $e; + throw new AuthException('That username is already taken'); } // Verify email address: diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth.php b/module/VuFind/src/VuFind/Auth/Shibboleth.php index ffbc6cf0163..57f49de977e 100644 --- a/module/VuFind/src/VuFind/Auth/Shibboleth.php +++ b/module/VuFind/src/VuFind/Auth/Shibboleth.php @@ -40,8 +40,6 @@ use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Service\ExternalSessionServiceInterface; use VuFind\Db\Service\UserCardServiceInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; use VuFind\Exception\Auth as AuthException; /** @@ -58,10 +56,8 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Page */ -class Shibboleth extends AbstractBase implements DbTableAwareInterface +class Shibboleth extends AbstractBase { - use DbTableAwareTrait; - /** * Header name for entityID of the IdP that authenticated the user. */ diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php index 8bd9e3b20d4..320658c62f7 100644 --- a/module/VuFind/src/VuFind/Controller/AbstractBase.php +++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php @@ -497,18 +497,6 @@ public function getRecordRouter() return $this->getService(\VuFind\Record\Router::class); } - /** - * Get a database table object. - * - * @param string $table Name of table to retrieve - * - * @return \VuFind\Db\Table\Gateway - */ - public function getTable($table) - { - return $this->getService(\VuFind\Db\Table\PluginManager::class)->get($table); - } - /** * Get a database service object. * diff --git a/module/VuFind/src/VuFind/Controller/InstallController.php b/module/VuFind/src/VuFind/Controller/InstallController.php index 760710d1bda..cf4247f8ab4 100644 --- a/module/VuFind/src/VuFind/Controller/InstallController.php +++ b/module/VuFind/src/VuFind/Controller/InstallController.php @@ -32,6 +32,7 @@ use Laminas\Mvc\MvcEvent; use VuFind\Config\Writer as ConfigWriter; use VuFind\Crypt\PasswordHasher; +use VuFind\Db\ConnectionFactory; use VuFind\Db\Service\TagServiceInterface; use VuFind\Db\Service\UserCardServiceInterface; use VuFind\Db\Service\UserServiceInterface; @@ -415,14 +416,15 @@ public function fixdatabaseAction() try { // We need a default database name to use to establish a connection: $dbName = ($view->driver == 'pgsql') ? 'template1' : 'mysql'; + $dbFactory = $this->serviceLocator->get(ConnectionFactory::class); $connectionParams = [ - 'driver' => $view->driver, - 'hostname' => $view->dbhost, - 'username' => $view->dbrootuser, + 'driver' => $dbFactory->getDriverName($view->driver), + 'host' => $view->dbhost, + 'user' => $view->dbrootuser, 'password' => $this->params()->fromPost('dbrootpass'), ]; - $db = $this->serviceLocator->get(\VuFind\Db\AdapterFactory::class)->getAdapterFromArray( - $connectionParams + ['database' => $dbName] + $db = $dbFactory->getConnectionFromOptions( + $connectionParams + ['dbname' => $dbName] ); } catch (\Exception $e) { $this->flashMessenger() @@ -438,7 +440,7 @@ public function fixdatabaseAction() // Get SQL together $escapedPass = $skip ? "'" . addslashes($newpass) . "'" - : $db->getPlatform()->quoteValue($newpass); + : $db->quote($newpass); $preCommands = $this->getPreCommands($view, $escapedPass); $postCommands = $this->getPostCommands($view); // We use the same file to initialize the MariaDB and MySQL databases: @@ -459,10 +461,10 @@ public function fixdatabaseAction() return $this->forwardTo('Install', 'showsql'); } else { foreach ($preCommands as $query) { - $db->query($query, $db::QUERY_MODE_EXECUTE); + $db->executeQuery($query); } - $db = $this->getService(\VuFind\Db\AdapterFactory::class)->getAdapterFromArray( - $connectionParams + ['database' => $view->dbname] + $db = $dbFactory->getConnectionFromOptions( + $connectionParams + ['dbname' => $view->dbname] ); $statements = explode(';', $sql); foreach ($statements as $current) { @@ -470,10 +472,10 @@ public function fixdatabaseAction() if (strlen(trim($current)) == 0) { continue; } - $db->query($current, $db::QUERY_MODE_EXECUTE); + $db->executeQuery($current); } foreach ($postCommands as $query) { - $db->query($query, $db::QUERY_MODE_EXECUTE); + $db->executeQuery($query); } // If we made it this far, we can update the config file and // forward back to the home action! diff --git a/module/VuFind/src/VuFind/Controller/UpgradeController.php b/module/VuFind/src/VuFind/Controller/UpgradeController.php index 11657ac0800..39fcd04d19b 100644 --- a/module/VuFind/src/VuFind/Controller/UpgradeController.php +++ b/module/VuFind/src/VuFind/Controller/UpgradeController.php @@ -34,7 +34,6 @@ use ArrayObject; use Composer\Semver\Comparator; use Exception; -use Laminas\Db\Adapter\Adapter; use Laminas\Mvc\MvcEvent; use Laminas\ServiceManager\ServiceLocatorInterface; use Laminas\Session\Container; @@ -47,7 +46,8 @@ use VuFind\Cookie\CookieManager; use VuFind\Crypt\Base62; use VuFind\Crypt\BlockCipher; -use VuFind\Db\AdapterFactory; +use VuFind\Db\Connection; +use VuFind\Db\ConnectionFactory; use VuFind\Db\MigrationManager; use VuFind\Db\Service\ResourceServiceInterface; use VuFind\Db\Service\ResourceTagsServiceInterface; @@ -61,6 +61,7 @@ use function count; use function dirname; +use function get_class; use function in_array; use function strlen; @@ -221,23 +222,23 @@ public function fixconfigAction() } /** - * Get a database adapter for root access using credentials in session. + * Get a database connection for root access using credentials in session. * - * @return Adapter + * @return Connection */ - protected function getRootDbAdapter() + protected function getRootDbConnection(): Connection { - // Use static cache to avoid loading adapter more than once on + // Use static cache to avoid loading connection more than once on // subsequent calls. - static $adapter = false; - if (!$adapter) { - $factory = $this->getService(AdapterFactory::class); - $adapter = $factory->getAdapter( + static $connection = false; + if (!$connection) { + $factory = $this->getService(ConnectionFactory::class); + $connection = $factory->getConnection( $this->session->dbRootUser, $this->session->dbRootPass ); } - return $adapter; + return $connection; } /** @@ -317,12 +318,9 @@ protected function fixSearchChecksumsInDatabase() */ public function getDatabaseMigrations(): string { - $adapter = $this->getService(Adapter::class); - $rawPlatform = strtolower($adapter->getDriver()->getDatabasePlatformName()); - $platform = match ($rawPlatform) { - 'postgresql' => 'pgsql', - default => $rawPlatform, - }; + $connection = $this->getService(Connection::class); + $rawPlatform = strtolower(get_class($connection->getDatabasePlatform())); + $platform = str_contains($rawPlatform, 'postgres') ? 'pgsql' : 'mysql'; $migrationManager = new MigrationManager(); $sql = ''; foreach ($migrationManager->getMigrations($platform, $this->cookie->oldVersion) as $migration) { @@ -344,11 +342,11 @@ public function applyDatabaseMigrations(): ?ViewModel if (!$this->hasDatabaseRootCredentials()) { return $this->forwardTo('Upgrade', 'GetDbCredentials'); } - $adapter = $this->getRootDbAdapter(); + $connection = $this->getRootDbConnection(); foreach (explode(';', $migrationSql) as $sqlLine) { $trimmedLine = trim($sqlLine); if (!empty($trimmedLine)) { - $adapter->query($trimmedLine, $adapter::QUERY_MODE_EXECUTE); + $connection->executeQuery($trimmedLine); } } // Don't keep DB credentials in session longer than necessary: @@ -465,9 +463,9 @@ public function getdbcredentialsAction() // Test the connection: try { // Query a table known to exist - $factory = $this->getService(AdapterFactory::class); - $db = $factory->getAdapter($dbrootuser, $pass); - $db->query('SELECT * FROM user;'); + $factory = $this->getService(ConnectionFactory::class); + $db = $factory->getConnection($dbrootuser, $pass); + $db->executeQuery('SELECT * FROM user;'); $this->session->dbRootUser = $dbrootuser; $this->session->dbRootPass = $pass; return $this->forwardTo('Upgrade', 'FixDatabase'); @@ -538,7 +536,10 @@ public function fixduplicatetagsAction() // Handle submit action: if ($this->formWasSubmitted()) { - $this->getService(TagsService::class)->fixDuplicateTags(); + $fixed = $this->getService(TagsService::class)->fixDuplicateTags(); + if ($fixed > 0) { + $this->session->warnings->append("Merged $fixed duplicate tag(s)"); + } return $this->forwardTo('Upgrade', 'FixDatabase'); } diff --git a/module/VuFind/src/VuFind/Db/Row/RowGateway.php b/module/VuFind/src/VuFind/Db/Connection.php similarity index 65% rename from module/VuFind/src/VuFind/Db/Row/RowGateway.php rename to module/VuFind/src/VuFind/Db/Connection.php index 5de9f560082..2bfd126abd5 100644 --- a/module/VuFind/src/VuFind/Db/Row/RowGateway.php +++ b/module/VuFind/src/VuFind/Db/Connection.php @@ -1,11 +1,11 @@ + * @package Db + * @author Aleksi Peebles * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -namespace VuFind\Db\Row; +namespace VuFind\Db; /** - * Abstract base class for DB rows. + * Wrapper class for VuFind Doctrine connections. * * @category VuFind - * @package Db_Row - * @author Demian Katz + * @package Db + * @author Aleksi Peebles * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -class RowGateway extends \Laminas\Db\RowGateway\RowGateway +class Connection extends \Doctrine\DBAL\Connection { - /** - * Retrieve primary key information. - * - * @return array - */ - public function getPrimaryKeyColumn() - { - return $this->primaryKeyColumn; - } } diff --git a/module/VuFind/src/VuFind/Db/AdapterFactory.php b/module/VuFind/src/VuFind/Db/ConnectionFactory.php similarity index 50% rename from module/VuFind/src/VuFind/Db/AdapterFactory.php rename to module/VuFind/src/VuFind/Db/ConnectionFactory.php index 554d052df9d..eda2357c240 100644 --- a/module/VuFind/src/VuFind/Db/AdapterFactory.php +++ b/module/VuFind/src/VuFind/Db/ConnectionFactory.php @@ -1,12 +1,12 @@ config = $config ?: new Config([]); } @@ -93,35 +113,51 @@ public function __invoke( throw new \Exception('Unexpected options sent to factory!'); } $this->config = $container->get(\VuFind\Config\PluginManager::class) - ->get('config'); - return $this->getAdapter(); + ->get($this->configName); + $this->container = $container; + return $this->getConnection(); } /** - * Obtain a Laminas\DB connection using standard VuFind configuration. + * Obtain a database connection using standard VuFind configuration. * * @param string $overrideUser Username override (leave null to use username * from config.ini) * @param string $overridePass Password override (leave null to use password * from config.ini) * - * @return Adapter + * @return Connection */ - public function getAdapter($overrideUser = null, $overridePass = null) + public function getConnection($overrideUser = null, $overridePass = null) { + // Make sure object cache is initialized; Doctrine needs it: + $this->container->get(\VuFind\Cache\Manager::class)->getCache('object'); + + // Parse details from connection string if available, otherwise use + // more granular config settings. if (isset($this->config->Database->database)) { - // Parse details from connection string: - return $this->getAdapterFromConnectionString( + $options = $this->getOptionsFromConnectionString( $this->config->Database->database, $overrideUser, $overridePass ); } else { - return $this->getAdapterFromConfig( - $overrideUser, - $overridePass - ); + $dbConfig = $this->config->Database ?? new Config([]); + $options = [ + 'driver' => $this->getDriverName($dbConfig->database_driver ?? ''), + 'host' => $dbConfig->database_host ?? null, + 'user' => $overrideUser ?? $dbConfig->database_username ?? null, + 'password' => $overridePass ?? $this->getSecretFromConfig($dbConfig, 'database_password'), + 'dbname' => $dbConfig->database_name ?? null, + ]; + if (!empty($dbConfig->database_port)) { + $options['port'] = $dbConfig->database_port; + } } + + $options['driverOptions'] = $this->getDriverOptions($options['driver']); + + return $this->getConnectionFromOptions($options); } /** @@ -134,14 +170,10 @@ public function getAdapter($overrideUser = null, $overridePass = null) public function getDriverName($type) { switch (strtolower($type)) { - // mariadb and mysql are equivalent for now: - case 'mariadb': case 'mysql': - return 'mysqli'; - case 'oci8': - return 'Oracle'; + return 'pdo_mysql'; case 'pgsql': - return 'Pdo_Pgsql'; + return 'pdo_pgsql'; } return $type; } @@ -155,72 +187,96 @@ public function getDriverName($type) */ protected function getDriverOptions($driver) { - switch ($driver) { - case 'mysqli': - return ($this->config->Database->verify_server_certificate ?? false) - ? [] : [MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]; + // Load options from the configuration: + $driverOptions = $this->config?->Database?->extra_options?->toArray() ?? []; + + // Apply MySQL-specific adjustments: + if ($driver == 'pdo_mysql') { + $driverOptions[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] + = $this->config->Database->verify_server_certificate ?? false; + $sslKeyMap = [ + 'client_key' => PDO::MYSQL_ATTR_SSL_KEY, + 'client_cert' => PDO::MYSQL_ATTR_SSL_CERT, + 'ca_cert' => PDO::MYSQL_ATTR_SSL_CA, + 'ca_path' => PDO::MYSQL_ATTR_SSL_CAPATH, + ]; + $sslConfigured = false; + foreach ($sslKeyMap as $oldKey => $newKey) { + if (isset($driverOptions[$oldKey])) { + $driverOptions[$newKey] = $driverOptions[$oldKey]; + unset($driverOptions[$oldKey]); + } + $sslConfigured = $sslConfigured || isset($driverOptions[$newKey]); + } + $useSsl = $this->config->Database->use_ssl ?? false; + if ($useSsl && !$sslConfigured) { + throw new \Exception( + 'To use SSL with MySQL, please configure appropriate extra_options in ' + . 'the [Database] section of config.ini.' + ); + } + if (!$useSsl && $sslConfigured) { + throw new \Exception( + 'Incompatible settings: SSL settings activated, but SSL disabled. ' + . 'See use_ssl and extra_options in config.ini [Database] section.' + ); + } } - return []; + + return $driverOptions; } /** - * Obtain a Laminas\DB connection using an option array. + * Obtain a database connection using an option array. * * @param array $options Options for building adapter * - * @return Adapter + * @return Connection */ - public function getAdapterFromOptions($options) + public function getConnectionFromOptions($options) { // Set up custom options by database type: $driver = strtolower($options['driver']); switch ($driver) { - case 'mysqli': + case 'pdo_mysql': $options['charset'] = $this->config->Database->charset ?? 'utf8mb4'; if (strtolower($options['charset']) === 'latin1') { throw new \Exception( - 'The latin1 encoding is no longer supported for MySQL' - . ' databases in VuFind. Please convert your database' - . ' to utf8 using VuFind 7.x or earlier BEFORE' - . ' upgrading to this version.' + 'The latin1 encoding is no longer supported for MySQL databases in VuFind.' ); } - $options['options'] = ['buffer_results' => true]; break; } + $options['wrapperClass'] = $this->wrapperClass; // Set up database connection: - $adapter = new Adapter($options); - - // Special-case setup: - if ($driver == 'pdo_pgsql' && isset($this->config->Database->schema)) { - // Set schema - $statement = $adapter->createStatement( - 'SET search_path TO ' . $this->config->Database->schema - ); - $statement->execute(); + if (empty($this->container)) { + throw new \Exception('Container is missing!'); } + $connection = DriverManager::getConnection( + $options + ); - return $adapter; + return $connection; } /** - * Obtain a Laminas\DB connection using a connection string. + * Parse database connection options from a connection string. * - * @param string $connectionString Connection string of the form + * @param string $connectionString Connection string of the form * [db_type]://[username]:[password]@[host]/[db_name] - * @param string $overrideUser Username override (leave null to use username + * @param ?string $overrideUser Username override (leave null to use username * from connection string) - * @param string $overridePass Password override (leave null to use password + * @param ?string $overridePass Password override (leave null to use password * from connection string) * - * @return Adapter + * @return array */ - public function getAdapterFromConnectionString( - $connectionString, - $overrideUser = null, - $overridePass = null - ) { + public function getOptionsFromConnectionString( + string $connectionString, + ?string $overrideUser = null, + ?string $overridePass = null + ): array { [$type, $details] = explode('://', $connectionString); preg_match('/(.+)@([^@]+)\/(.+)/', $details, $matches); $credentials = $matches[1] ?? null; @@ -242,70 +298,19 @@ public function getAdapterFromConnectionString( $username = $overrideUser ?? $username; $password = $overridePass ?? $password; - return $this->getAdapterFromArray([ - 'driver' => $type, - 'hostname' => $host, - 'username' => $username, - 'password' => $password, - 'database' => $dbName, - 'use_ssl' => $this->config->Database->use_ssl ?? false, - 'port' => $port ?? null, - ]); - } - - /** - * Obtain a Laminas\DB connection using the config. - * - * @param string $overrideUser Username override (leave null to use username from config) - * @param string $overridePass Password override (leave null to use password from config) - * - * @return Adapter - */ - public function getAdapterFromConfig($overrideUser = null, $overridePass = null) - { - if (!isset($this->config->Database)) { - throw new \Exception('[Database] Configuration section missing'); - } - $config = $this->config->Database; - return $this->getAdapterFromArray([ - 'driver' => $config->database_driver ?? null, - 'hostname' => $config->database_host ?? null, - 'username' => $overrideUser ?? $config->database_username ?? null, - 'password' => $overridePass ?? $this->getSecretFromConfig($config, 'database_password'), - 'database' => $config->database_name, - 'port' => $config->database_port ?? null, - ]); - } - - /** - * Obtain a Laminas\DB connection using a set of given element. - * - * @param array $config Config array to connect to the DB containing - * driver (ie: mysql), username, password, hostname, database (db name), port - * - * @return Adapter - */ - public function getAdapterFromArray(array $config) - { - $driverName = $this->getDriverName($config['driver']); + $driverName = $this->getDriverName($type); // Set up default options: $options = [ 'driver' => $driverName, - 'hostname' => $config['hostname'] ?? null, - 'username' => $config['username'] ?? null, - 'password' => $config['password'] ?? null, - 'database' => $config['database'] ?? null, - 'use_ssl' => $this->config->Database->use_ssl ?? false, - 'driver_options' => $this->getDriverOptions($driverName), + 'host' => $host, + 'user' => $username, + 'password' => $password, + 'dbname' => $dbName, ]; - if (isset($config['port'])) { - $options['port'] = $config['port']; + if (!empty($port)) { + $options['port'] = $port; } - // Get extra custom options from config: - $extraOptions = $this->config?->Database?->extra_options?->toArray() ?? []; - // Note: $options takes precedence over $extraOptions -- we don't want users - // using extended settings to override values from core settings. - return $this->getAdapterFromOptions($options + $extraOptions); + return $options; } } diff --git a/module/VuFind/src/VuFind/Db/Table/UserFactory.php b/module/VuFind/src/VuFind/Db/ConnectionFactoryFactory.php similarity index 83% rename from module/VuFind/src/VuFind/Db/Table/UserFactory.php rename to module/VuFind/src/VuFind/Db/ConnectionFactoryFactory.php index 05f39e564f6..aecf835efe1 100644 --- a/module/VuFind/src/VuFind/Db/Table/UserFactory.php +++ b/module/VuFind/src/VuFind/Db/ConnectionFactoryFactory.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki + * @link https://vufind.org Main Site */ -namespace VuFind\Db\Table; +namespace VuFind\Db; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface as ContainerException; use Psr\Container\ContainerInterface; /** - * User table gateway factory. + * Factory for Doctrine connection factory. * * @category VuFind - * @package Db_Table + * @package Service * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development Wiki */ -class UserFactory extends GatewayFactory +class ConnectionFactoryFactory implements FactoryInterface { /** * Create an object @@ -65,6 +66,6 @@ public function __invoke( ?array $options = null ) { $config = $container->get(\VuFind\Config\PluginManager::class)->get('config'); - return parent::__invoke($container, $requestedName, [$config]); + return new $requestedName($config, $container, ...($options ?: [])); } } diff --git a/module/VuFind/src/VuFind/Db/Entity/AccessToken.php b/module/VuFind/src/VuFind/Db/Entity/AccessToken.php new file mode 100644 index 00000000000..1d09ad2e6cf --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/AccessToken.php @@ -0,0 +1,251 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; + +/** + * Entity model for access_token table + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'access_token')] +#[ORM\Index(name: 'access_token_user_id_idx', columns: ['user_id'])] +#[ORM\Entity] +class AccessToken implements AccessTokenEntityInterface +{ + use DateTimeTrait; + + /** + * Unique ID. + * + * @var string + */ + #[ORM\Column(name: 'id', type: 'string', length: 255, nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'NONE')] + protected string $id; + + /** + * Token type. + * + * @var string + */ + #[ORM\Column(name: 'type', type: 'string', length: 128, nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'NONE')] + protected string $type; + + /** + * User. + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Data. + * + * @var ?string + */ + #[ORM\Column(name: 'data', type: 'text', length: 16777215, nullable: true)] + protected ?string $data = null; + + /** + * Flag indicating status of the token. + * + * @var bool + */ + #[ORM\Column(name: 'revoked', type: 'boolean', nullable: false, options: ['default' => false])] + protected bool $revoked = false; + + /** + * Constructor. + */ + public function __construct() + { + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); + } + + /** + * Set access token identifier. + * + * @param string $id Access Token Identifier + * + * @return static + */ + public function setId(string $id): static + { + $this->id = $id; + return $this; + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?string + */ + public function getId(): ?string + { + return $this->id; + } + + /** + * Get type of access token. + * + * @return ?string + */ + public function getType(): ?string + { + return $this->type; + } + + /** + * Set type of access token. + * + * @param ?string $type Access Token Type + * + * @return static + */ + public function setType(?string $type): static + { + $this->type = $type; + return $this; + } + + /** + * Set user. + * + * @param ?UserEntityInterface $user User owning token + * + * @return static + */ + public function setUser(?UserEntityInterface $user): static + { + $this->user = $user; + return $this; + } + + /** + * Get user ID. + * + * @return ?UserEntityInterface + */ + public function getUser(): ?UserEntityInterface + { + return $this->user; + } + + /** + * Get created date. + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Set created date. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Get data. + * + * @return ?string + */ + public function getData(): ?string + { + return $this->data; + } + + /** + * Set data. + * + * @param ?string $data Data + * + * @return static + */ + public function setData(?string $data): static + { + $this->data = $data; + return $this; + } + + /** + * Is the access token revoked? + * + * @return bool + */ + public function isRevoked(): bool + { + return $this->revoked; + } + + /** + * Set revoked status. + * + * @param bool $revoked Revoked + * + * @return static + */ + public function setRevoked(bool $revoked): static + { + $this->revoked = $revoked; + return $this; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php index baa8bd89cfb..b0a6e40f20b 100644 --- a/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php @@ -29,6 +29,8 @@ namespace VuFind\Db\Entity; +use DateTime; + /** * Entity model interface for access tokens. * @@ -41,7 +43,39 @@ interface AccessTokenEntityInterface extends EntityInterface { /** - * Set user ID. + * Set access token identifier. + * + * @param string $id Access Token Identifier + * + * @return static + */ + public function setId(string $id): static; + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?string + */ + public function getId(): ?string; + + /** + * Get type of access token. + * + * @return ?string + */ + public function getType(): ?string; + + /** + * Set type of access token. + * + * @param ?string $type Access Token Type + * + * @return static + */ + public function setType(?string $type): static; + + /** + * Set user. * * @param ?UserEntityInterface $user User owning token * @@ -49,14 +83,44 @@ interface AccessTokenEntityInterface extends EntityInterface */ public function setUser(?UserEntityInterface $user): static; + /** + * Get user. + * + * @return ?UserEntityInterface + */ + public function getUser(): ?UserEntityInterface; + + /** + * Get created date. + * + * @return DateTime + */ + public function getCreated(): DateTime; + + /** + * Set created date. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static; + + /** + * Get data. + * + * @return ?string + */ + public function getData(): ?string; + /** * Set data. * - * @param string $data Data + * @param ?string $data Data * * @return static */ - public function setData(string $data): static; + public function setData(?string $data): static; /** * Is the access token revoked? diff --git a/module/VuFind/src/VuFind/Db/Row/AuthHash.php b/module/VuFind/src/VuFind/Db/Entity/AuthHash.php similarity index 56% rename from module/VuFind/src/VuFind/Db/Row/AuthHash.php rename to module/VuFind/src/VuFind/Db/Entity/AuthHash.php index de5a60da0ac..aa323874994 100644 --- a/module/VuFind/src/VuFind/Db/Row/AuthHash.php +++ b/module/VuFind/src/VuFind/Db/Entity/AuthHash.php @@ -1,12 +1,11 @@ - * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\AuthHashEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for auth_hash + * AuthHash * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz - * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $session_id - * @property string $hash - * @property string $type - * @property string $data - * @property string $created + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class AuthHash extends RowGateway implements AuthHashEntityInterface, DbServiceAwareInterface +#[ORM\Table(name: 'auth_hash')] +#[ORM\Index(name: 'auth_hash_created_idx', columns: ['created'])] +#[ORM\Index(name: 'auth_hash_session_id_idx', columns: ['session_id'])] +#[ORM\UniqueConstraint(name: 'auth_hash_hash_type_idx', columns: ['hash', 'type'], options: ['lengths' => [140, null]])] +#[ORM\Entity] +class AuthHash implements AuthHashEntityInterface { - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + */ + #[ORM\Id] + #[ORM\Column(name: 'id', type: 'bigint', nullable: false, options: ['unsigned' => true])] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Session ID. + * + * @var ?string + */ + #[ORM\Column(name: 'session_id', type: 'string', length: 128, nullable: true)] + protected ?string $sessionId = null; + + /** + * Hash value. + * + * @var string + */ + #[ORM\Column(name: 'hash', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $hash = ''; /** - * Constructor + * Type of the hash. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var ?string + */ + #[ORM\Column(name: 'type', type: 'string', length: 50, nullable: true)] + protected ?string $type = null; + + /** + * Data. + * + * @var ?string + */ + #[ORM\Column(name: 'data', type: 'text', length: 16777215, nullable: true)] + protected ?string $data = null; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $created; + + /** + * Constructor. */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'auth_hash', $adapter); + // Set the default value as a DateTime object + $this->created = new Datetime(); } /** @@ -75,7 +114,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -85,7 +124,7 @@ public function getId(): ?int */ public function getSessionId(): ?string { - return $this->session_id ?? null; + return $this->sessionId; } /** @@ -97,7 +136,7 @@ public function getSessionId(): ?string */ public function setSessionId(?string $sessionId): static { - $this->session_id = $sessionId; + $this->sessionId = $sessionId; return $this; } @@ -108,7 +147,7 @@ public function setSessionId(?string $sessionId): static */ public function getHash(): string { - return $this->hash ?? ''; + return $this->hash; } /** @@ -131,7 +170,7 @@ public function setHash(string $hash): static */ public function getHashType(): ?string { - return $this->type ?? null; + return $this->type; } /** @@ -154,7 +193,7 @@ public function setHashType(?string $type): static */ public function getData(): ?string { - return $this->__get('data'); + return $this->data; } /** @@ -166,7 +205,7 @@ public function getData(): ?string */ public function setData(?string $data): static { - $this->__set('data', $data); + $this->data = $data; return $this; } @@ -177,7 +216,7 @@ public function setData(?string $data): static */ public function getCreated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); + return $this->created; } /** @@ -189,7 +228,7 @@ public function getCreated(): DateTime */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php b/module/VuFind/src/VuFind/Db/Entity/ChangeTracker.php similarity index 59% rename from module/VuFind/src/VuFind/Db/Row/ChangeTracker.php rename to module/VuFind/src/VuFind/Db/Entity/ChangeTracker.php index e3f15904e1c..510b9e549b7 100644 --- a/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php +++ b/module/VuFind/src/VuFind/Db/Entity/ChangeTracker.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\ChangeTrackerEntityInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for change_tracker + * ChangeTracker * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property string $core - * @property string $id - * @property ?string $first_indexed - * @property ?string $last_indexed - * @property ?string $last_record_change - * @property ?string $deleted + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class ChangeTracker extends RowGateway implements ChangeTrackerEntityInterface +#[ORM\Table(name: 'change_tracker')] +#[ORM\Index(name: 'change_tracker_deleted_idx', columns: ['deleted'])] +#[ORM\Entity] +class ChangeTracker implements ChangeTrackerEntityInterface { /** - * Constructor + * Solr core containing record. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var string */ - public function __construct($adapter) - { - parent::__construct(['core', 'id'], 'change_tracker', $adapter); - } + #[ORM\Column(name: 'core', type: 'string', length: 30, nullable: false)] + #[ORM\Id] + protected string $core; + + /** + * Id of record within core. + * + * @var string + */ + #[ORM\Column(name: 'id', type: 'string', length: 120, nullable: false)] + #[ORM\Id] + protected string $id; + + /** + * First time added to index + * + * @var ?DateTime + */ + #[ORM\Column(name: 'first_indexed', type: 'datetime', nullable: true)] + protected ?DateTime $firstIndexed = null; + + /** + * Last time changed in index. + * + * @var ?DateTime + */ + #[ORM\Column(name: 'last_indexed', type: 'datetime', nullable: true)] + protected ?DateTime $lastIndexed = null; + + /** + * Last time original record was edited. + * + * @var ?DateTime + */ + #[ORM\Column(name: 'last_record_change', type: 'datetime', nullable: true)] + protected ?DateTime $lastRecordChange = null; + + /** + * Time record was removed from index. + * + * @var ?DateTime + */ + #[ORM\Column(name: 'deleted', type: 'datetime', nullable: true)] + protected ?DateTime $deleted = null; /** * Setter for identifier. @@ -74,13 +110,13 @@ public function setId(string $id): static } /** - * Getter for identifier. + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return string + * @return ?string */ - public function getId(): string + public function getId(): ?string { - return $this->id; + return $this->id ?? null; } /** @@ -115,7 +151,7 @@ public function getIndexName(): string */ public function setFirstIndexed(?DateTime $dateTime): static { - $this->first_indexed = $dateTime->format('Y-m-d H:i:s'); + $this->firstIndexed = $dateTime; return $this; } @@ -126,7 +162,7 @@ public function setFirstIndexed(?DateTime $dateTime): static */ public function getFirstIndexed(): ?DateTime { - return $this->first_indexed ? DateTime::createFromFormat('Y-m-d H:i:s', $this->first_indexed) : null; + return $this->firstIndexed; } /** @@ -138,7 +174,7 @@ public function getFirstIndexed(): ?DateTime */ public function setLastIndexed(?DateTime $dateTime): static { - $this->last_indexed = $dateTime->format('Y-m-d H:i:s'); + $this->lastIndexed = $dateTime; return $this; } @@ -149,7 +185,7 @@ public function setLastIndexed(?DateTime $dateTime): static */ public function getLastIndexed(): ?DateTime { - return $this->last_indexed ? DateTime::createFromFormat('Y-m-d H:i:s', $this->last_indexed) : null; + return $this->lastIndexed; } /** @@ -161,7 +197,7 @@ public function getLastIndexed(): ?DateTime */ public function setLastRecordChange(?DateTime $dateTime): static { - $this->last_record_change = $dateTime->format('Y-m-d H:i:s'); + $this->lastRecordChange = $dateTime; return $this; } @@ -172,7 +208,7 @@ public function setLastRecordChange(?DateTime $dateTime): static */ public function getLastRecordChange(): ?DateTime { - return $this->last_record_change ? DateTime::createFromFormat('Y-m-d H:i:s', $this->last_record_change) : null; + return $this->lastRecordChange; } /** @@ -184,7 +220,7 @@ public function getLastRecordChange(): ?DateTime */ public function setDeleted(?DateTime $dateTime): static { - $this->deleted = $dateTime->format('Y-m-d H:i:s'); + $this->deleted = $dateTime; return $this; } @@ -195,6 +231,6 @@ public function setDeleted(?DateTime $dateTime): static */ public function getDeleted(): ?DateTime { - return $this->deleted ? DateTime::createFromFormat('Y-m-d H:i:s', $this->deleted) : null; + return $this->deleted; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/ChangeTrackerEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/ChangeTrackerEntityInterface.php index 90afc00a5d2..f3e014ec425 100644 --- a/module/VuFind/src/VuFind/Db/Entity/ChangeTrackerEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/ChangeTrackerEntityInterface.php @@ -52,11 +52,11 @@ interface ChangeTrackerEntityInterface extends EntityInterface public function setId(string $id): static; /** - * Getter for identifier. + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return string + * @return ?string */ - public function getId(): string; + public function getId(): ?string; /** * Setter for index name (formerly core). diff --git a/module/VuFind/src/VuFind/Db/Entity/Comments.php b/module/VuFind/src/VuFind/Db/Entity/Comments.php new file mode 100644 index 00000000000..666584544f7 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Comments.php @@ -0,0 +1,203 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; + +/** + * Comments + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'comments')] +#[ORM\Index(name: 'comments_user_id_idx', columns: ['user_id'])] +#[ORM\Index(name: 'comments_resource_id_idx', columns: ['resource_id'])] +#[ORM\Entity] +class Comments implements CommentsEntityInterface +{ + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Comment. + * + * @var string + */ + #[ORM\Column(name: 'comment', type: 'text', length: 65535, nullable: false)] + protected string $comment; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * User ID. + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * Resource ID. + * + * @var ResourceEntityInterface + */ + #[ORM\JoinColumn( + name: 'resource_id', + referencedColumnName: 'id', + nullable: false, + options: ['default' => 0], + onDelete: 'CASCADE' + )] + #[ORM\ManyToOne(targetEntity: ResourceEntityInterface::class)] + protected ResourceEntityInterface $resource; + + /** + * Constructor. + */ + public function __construct() + { + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Comment setter + * + * @param string $comment Comment + * + * @return static + */ + public function setComment(string $comment): static + { + $this->comment = $comment; + return $this; + } + + /** + * Comment getter + * + * @return string + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * Created setter. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Created getter + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * User setter. + * + * @param ?UserEntityInterface $user User that created comment + * + * @return static + */ + public function setUser(?UserEntityInterface $user): static + { + $this->user = $user; + return $this; + } + + /** + * User getter + * + * @return ?UserEntityInterface + */ + public function getUser(): ?UserEntityInterface + { + return $this->user; + } + + /** + * Resource setter. + * + * @param ResourceEntityInterface $resource Resource + * + * @return static + */ + public function setResource(ResourceEntityInterface $resource): static + { + $this->resource = $resource; + return $this; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/CommentsEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/CommentsEntityInterface.php index 892c68632a7..2232872165a 100644 --- a/module/VuFind/src/VuFind/Db/Entity/CommentsEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/CommentsEntityInterface.php @@ -43,11 +43,11 @@ interface CommentsEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Comment setter @@ -100,7 +100,7 @@ public function getUser(): ?UserEntityInterface; /** * Resource setter. * - * @param ResourceEntityInterface $resource Resource id. + * @param ResourceEntityInterface $resource Resource * * @return static */ diff --git a/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php b/module/VuFind/src/VuFind/Db/Entity/ExchangeArrayInterface.php similarity index 65% rename from module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php rename to module/VuFind/src/VuFind/Db/Entity/ExchangeArrayInterface.php index 1d321df7a70..0309ea0428d 100644 --- a/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/ExchangeArrayInterface.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -namespace VuFind\Db\Table; +namespace VuFind\Db\Entity; /** - * Marker interface for classes that depend on the \VuFind\Db\Table\PluginManager + * Interface for translating an entity to and from array format. * * @category VuFind - * @package Db_Table + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -interface DbTableAwareInterface +interface ExchangeArrayInterface extends EntityInterface { /** - * Get the plugin manager. Throw an exception if it is missing. + * Populate entity data from an associative array. * - * @throws \Exception - * @return PluginManager + * @param array $data Key-value pairs representing entity properties. + * + * @return void */ - public function getDbTableManager(); + public function exchangeArray(array $data): void; /** - * Set the plugin manager. + * Get an array representation of the entity. * - * @param PluginManager $manager Plugin manager - * - * @return void + * @return array */ - public function setDbTableManager(PluginManager $manager); + public function toArray(): array; } diff --git a/module/VuFind/src/VuFind/Db/Table/PluginFactory.php b/module/VuFind/src/VuFind/Db/Entity/ExchangeArrayTrait.php similarity index 56% rename from module/VuFind/src/VuFind/Db/Table/PluginFactory.php rename to module/VuFind/src/VuFind/Db/Entity/ExchangeArrayTrait.php index a936ada53a9..a1bba8a61cd 100644 --- a/module/VuFind/src/VuFind/Db/Table/PluginFactory.php +++ b/module/VuFind/src/VuFind/Db/Entity/ExchangeArrayTrait.php @@ -1,11 +1,11 @@ + * @package Database + * @author Padmasree Gade * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Table; +namespace VuFind\Db\Entity; /** - * Database table plugin factory + * Trait providing a basic implementation of ExchangeArrayInterface. * * @category VuFind - * @package Db_Table + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class PluginFactory extends \VuFind\ServiceManager\AbstractPluginFactory +trait ExchangeArrayTrait { /** - * Constructor + * Populate entity data from an associative array. + * + * @param array $data Key-value pairs representing entity properties. + * + * @return void + */ + public function exchangeArray(array $data): void + { + foreach ($data as $key => $value) { + if (property_exists($this, $key)) { + $this->$key = $value; + } + } + } + + /** + * Get an array representation of the entity. + * + * @return array */ - public function __construct() + public function toArray(): array { - $this->defaultNamespace = 'VuFind\Db\Table'; + return get_object_vars($this); } } diff --git a/module/VuFind/src/VuFind/Db/Row/ExternalSession.php b/module/VuFind/src/VuFind/Db/Entity/ExternalSession.php similarity index 53% rename from module/VuFind/src/VuFind/Db/Row/ExternalSession.php rename to module/VuFind/src/VuFind/Db/Entity/ExternalSession.php index ad46955a5fa..450b38161b7 100644 --- a/module/VuFind/src/VuFind/Db/Row/ExternalSession.php +++ b/module/VuFind/src/VuFind/Db/Entity/ExternalSession.php @@ -1,12 +1,11 @@ - * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\ExternalSessionEntityInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for external_session + * ExternalSession * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz - * @author Ere Maijala * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $session_id - * @property string $external_session_id - * @property string $created + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class ExternalSession extends RowGateway implements ExternalSessionEntityInterface +#[ORM\Table(name: 'external_session')] +#[ORM\Index(name: 'external_session_id_idx', columns: ['external_session_id'], options: ['lengths' => [190]])] +#[ORM\UniqueConstraint(name: 'external_session_session_id_idx', columns: ['session_id'])] +#[ORM\Entity] +class ExternalSession implements ExternalSessionEntityInterface { + use DateTimeTrait; + /** - * Constructor + * Unique ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var int + */ + #[ORM\Column(name: 'id', type: 'bigint', nullable: false, options: ['unsigned' => true])] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Session ID. + * + * @var string + */ + #[ORM\Column(name: 'session_id', type: 'string', length: 128, nullable: false)] + protected string $sessionId; + + /** + * External session ID. + * + * @var string + */ + #[ORM\Column(name: 'external_session_id', type: 'string', length: 255, nullable: false)] + protected string $externalSessionId; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Constructor. */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'external_session', $adapter); + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); } /** @@ -68,7 +100,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -78,7 +110,7 @@ public function getId(): ?int */ public function getSessionId(): string { - return $this->session_id ?? ''; + return $this->sessionId; } /** @@ -90,18 +122,18 @@ public function getSessionId(): string */ public function setSessionId(string $sessionId): static { - $this->session_id = $sessionId; + $this->sessionId = $sessionId; return $this; } /** - * Get PHP external session id string. + * Get external session id string. * * @return string */ public function getExternalSessionId(): string { - return $this->external_session_id ?? ''; + return $this->externalSessionId; } /** @@ -113,7 +145,7 @@ public function getExternalSessionId(): string */ public function setExternalSessionId(string $externalSessionId): static { - $this->external_session_id = $externalSessionId; + $this->externalSessionId = $externalSessionId; return $this; } @@ -124,7 +156,7 @@ public function setExternalSessionId(string $externalSessionId): static */ public function getCreated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); + return $this->created; } /** @@ -136,7 +168,7 @@ public function getCreated(): DateTime */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/Feedback.php b/module/VuFind/src/VuFind/Db/Entity/Feedback.php new file mode 100644 index 00000000000..ecda3715e83 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Feedback.php @@ -0,0 +1,363 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; + +/** + * Entity model for feedback table + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'feedback')] +#[ORM\Index(name: 'feedback_user_id_idx', columns: ['user_id'])] +#[ORM\Index(name: 'feedback_created_idx', columns: ['created'])] +#[ORM\Index(name: 'feedback_status_idx', columns: ['status'], options: ['lengths' => [191]])] +#[ORM\Index(name: 'feedback_form_name_idx', columns: ['form_name'], options: ['lengths' => [191]])] +#[ORM\Index(name: 'feedback_updated_by_idx', columns: ['updated_by'])] +#[ORM\Entity] +class Feedback implements FeedbackEntityInterface +{ + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false, options: ['unsigned' => true])] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Message + * + * @var string + */ + #[ORM\Column(name: 'message', type: 'text', nullable: true)] + protected string $message; + + /** + * Form data + * + * @var ?array + */ + #[ORM\Column(name: 'form_data', type: 'json', length: 0, nullable: true)] + protected ?array $formData = null; + + /** + * Form name + * + * @var string + */ + #[ORM\Column(name: 'form_name', type: 'string', length: 255, nullable: false)] + protected string $formName; + + /** + * Creation date + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $created; + + /** + * Last update date + * + * @var DateTime + */ + #[ORM\Column(name: 'updated', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $updated; + + /** + * Status + * + * @var string + */ + #[ORM\Column(name: 'status', type: 'string', length: 255, nullable: false, options: ['default' => 'open'])] + protected string $status = 'open'; + + /** + * Site URL + * + * @var string + */ + #[ORM\Column(name: 'site_url', type: 'string', length: 255, nullable: false)] + protected string $siteUrl; + + /** + * User that created request + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * User that updated request + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'updated_by', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $updatedBy = null; + + /** + * Constructor. + */ + public function __construct() + { + // Set the default value as a DateTime object + $this->created = new Datetime(); + $this->updated = new Datetime(); + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Message setter + * + * @param string $message Message + * + * @return static + */ + public function setMessage(string $message): static + { + $this->message = $message; + return $this; + } + + /** + * Message getter + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Form data setter. + * + * @param ?array $data Form data + * + * @return static + */ + public function setFormData(?array $data): static + { + $this->formData = $data; + return $this; + } + + /** + * Form data getter + * + * @return ?array + */ + public function getFormData(): ?array + { + return $this->formData; + } + + /** + * Form name setter. + * + * @param string $name Form name + * + * @return static + */ + public function setFormName(string $name): static + { + $this->formName = $name; + return $this; + } + + /** + * Form name getter + * + * @return string + */ + public function getFormName(): string + { + return $this->formName; + } + + /** + * Created setter. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Created getter + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Updated setter. + * + * @param DateTime $dateTime Last update date + * + * @return static + */ + public function setUpdated(DateTime $dateTime): static + { + $this->updated = $dateTime; + return $this; + } + + /** + * Updated getter + * + * @return DateTime + */ + public function getUpdated(): DateTime + { + return $this->updated; + } + + /** + * Status setter. + * + * @param string $status Status + * + * @return static + */ + public function setStatus(string $status): static + { + $this->status = $status; + return $this; + } + + /** + * Status getter + * + * @return string + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * Site URL setter. + * + * @param string $url Site URL + * + * @return static + */ + public function setSiteUrl(string $url): static + { + $this->siteUrl = $url; + return $this; + } + + /** + * Site URL getter + * + * @return string + */ + public function getSiteUrl(): string + { + return $this->siteUrl; + } + + /** + * User setter. + * + * @param ?UserEntityInterface $user User that created request + * + * @return static + */ + public function setUser(?UserEntityInterface $user): static + { + $this->user = $user; + return $this; + } + + /** + * User getter + * + * @return ?UserEntityInterface + */ + public function getUser(): ?UserEntityInterface + { + return $this->user; + } + + /** + * Updatedby setter. + * + * @param ?UserEntityInterface $user User that updated request + * + * @return static + */ + public function setUpdatedBy(?UserEntityInterface $user): static + { + $this->updatedBy = $user; + return $this; + } + + /** + * Updatedby getter + * + * @return ?UserEntityInterface + */ + public function getUpdatedBy(): ?UserEntityInterface + { + return $this->updatedBy; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/FeedbackEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/FeedbackEntityInterface.php index 4f75f29fa77..1056e0ba963 100644 --- a/module/VuFind/src/VuFind/Db/Entity/FeedbackEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/FeedbackEntityInterface.php @@ -43,11 +43,11 @@ interface FeedbackEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Message setter @@ -68,18 +68,18 @@ public function getMessage(): string; /** * Form data setter. * - * @param array $data Form data + * @param ?array $data Form data * * @return static */ - public function setFormData(array $data): static; + public function setFormData(?array $data): static; /** * Form data getter * - * @return array + * @return ?array */ - public function getFormData(): array; + public function getFormData(): ?array; /** * Form name setter. diff --git a/module/VuFind/src/VuFind/Db/Row/LoginToken.php b/module/VuFind/src/VuFind/Db/Entity/LoginToken.php similarity index 58% rename from module/VuFind/src/VuFind/Db/Row/LoginToken.php rename to module/VuFind/src/VuFind/Db/Entity/LoginToken.php index 645b8ea027d..0b7c7504789 100644 --- a/module/VuFind/src/VuFind/Db/Row/LoginToken.php +++ b/module/VuFind/src/VuFind/Db/Entity/LoginToken.php @@ -1,11 +1,11 @@ + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\LoginTokenEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\UserServiceInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for login_token + * Entity model for login_token table * * @category VuFind - * @package Db_Row - * @author Jaro Ravila + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property string $token - * @property string $series - * @property string $last_login - * @property ?string $browser - * @property ?string $platform - * @property int $expires - * @property string $last_session_id + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class LoginToken extends RowGateway implements DbServiceAwareInterface, LoginTokenEntityInterface +#[ORM\Table(name: 'login_token')] +#[ORM\Index(name: 'login_token_user_id_idx', columns: ['user_id'])] +#[ORM\Index(name: 'login_token_series_idx', columns: ['series'])] +#[ORM\Entity] +class LoginToken implements LoginTokenEntityInterface { - use DbServiceAwareTrait; + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; /** - * Constructor + * User ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var UserEntityInterface */ - public function __construct($adapter) + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected UserEntityInterface $user; + + /** + * Token. + * + * @var string + */ + #[ORM\Column(name: 'token', type: 'string', length: 255, nullable: false)] + protected string $token; + + /** + * Series. + * + * @var string + */ + #[ORM\Column(name: 'series', type: 'string', length: 255, nullable: false)] + protected string $series; + + /** + * Last login date. + * + * @var DateTime + */ + #[ORM\Column(name: 'last_login', type: 'datetime', nullable: false)] + protected DateTime $lastLogin; + + /** + * Browser. + * + * @var ?string + */ + #[ORM\Column(name: 'browser', type: 'string', length: 255, nullable: true)] + protected ?string $browser = null; + + /** + * Platform. + * + * @var ?string + */ + #[ORM\Column(name: 'platform', type: 'string', length: 255, nullable: true)] + protected ?string $platform = null; + + /** + * Expires. + * + * @var int + */ + #[ORM\Column(name: 'expires', type: 'integer', nullable: false)] + protected int $expires; + + /** + * Last session ID. + * + * @var ?string + */ + #[ORM\Column(name: 'last_session_id', type: 'string', length: 255, nullable: true)] + protected ?string $lastSessionId = null; + + /** + * Constructor. + */ + public function __construct() { - parent::__construct('id', 'login_token', $adapter); + // Set the default value as a DateTime object + $this->lastLogin = $this->getUnassignedDefaultDateTime(); } /** - * Getter for ID. + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int + public function getId(): ?int { - return $this->id; + return $this->id ?? null; } /** @@ -88,7 +153,7 @@ public function getId(): int */ public function setUser(UserEntityInterface $user): static { - $this->user_id = $user->getId(); + $this->user = $user; return $this; } @@ -99,9 +164,7 @@ public function setUser(UserEntityInterface $user): static */ public function getUser(): ?UserEntityInterface { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; + return $this->user ?? null; } /** @@ -159,7 +222,7 @@ public function getSeries(): string */ public function setLastLogin(DateTime $dateTime): static { - $this->last_login = $dateTime->format('Y-m-d H:i:s'); + $this->lastLogin = $dateTime; return $this; } @@ -170,7 +233,7 @@ public function setLastLogin(DateTime $dateTime): static */ public function getLastLogin(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->last_login); + return $this->lastLogin; } /** @@ -251,7 +314,7 @@ public function getExpires(): int */ public function setLastSessionId(?string $sid): static { - $this->last_session_id = $sid; + $this->lastSessionId = $sid; return $this; } @@ -262,6 +325,6 @@ public function setLastSessionId(?string $sid): static */ public function getLastSessionId(): ?string { - return $this->last_session_id; + return $this->lastSessionId; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/LoginTokenEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/LoginTokenEntityInterface.php index b6be0b860aa..d6c0bc655ee 100644 --- a/module/VuFind/src/VuFind/Db/Entity/LoginTokenEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/LoginTokenEntityInterface.php @@ -43,11 +43,11 @@ interface LoginTokenEntityInterface extends EntityInterface { /** - * Getter for ID. + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Setter for User. diff --git a/module/VuFind/src/VuFind/Db/Row/OaiResumption.php b/module/VuFind/src/VuFind/Db/Entity/OaiResumption.php similarity index 56% rename from module/VuFind/src/VuFind/Db/Row/OaiResumption.php rename to module/VuFind/src/VuFind/Db/Entity/OaiResumption.php index 489f543df01..fe12343d3bd 100644 --- a/module/VuFind/src/VuFind/Db/Row/OaiResumption.php +++ b/module/VuFind/src/VuFind/Db/Entity/OaiResumption.php @@ -1,11 +1,11 @@ * @author Juha Luoma * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\OaiResumptionEntityInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for oai_resumption + * OaiResumption * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @author Juha Luoma * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $params - * @property string $token - * @property string $expires + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class OaiResumption extends RowGateway implements OaiResumptionEntityInterface +#[ORM\Table(name: 'oai_resumption')] +#[ORM\UniqueConstraint(name: 'oai_resumption_token_idx', columns: ['token'])] +#[ORM\Entity] +class OaiResumption implements OaiResumptionEntityInterface { + use DateTimeTrait; + /** - * Constructor + * Unique ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var int */ - public function __construct($adapter) - { - parent::__construct('id', 'oai_resumption', $adapter); - } + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; /** - * Extract an array of parameters from the object. + * Resumption parameters. * - * @return array Original saved parameters. - * - * @deprecated Use parse_str() instead + * @var ?string */ - public function restoreParams() - { - $parts = explode('&', $this->params); - $params = []; - foreach ($parts as $part) { - [$key, $value] = explode('=', $part); - $key = urldecode($key); - $value = urldecode($value); - $params[$key] = $value; - } - return $params; - } + #[ORM\Column(name: 'params', type: 'text', length: 65535, nullable: true)] + protected ?string $params = null; /** - * Encode an array of parameters into the object. + * Expiry date. * - * @param array $params Parameters to save. - * - * @return void + * @var DateTime + */ + #[ORM\Column(name: 'expires', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $expires; + + /** + * Token. * - * @deprecated Use \VuFind\Db\Service\OaiResumptionService::createAndPersistToken() + * @var ?string + */ + #[ORM\Column(name: 'token', type: 'string', length: 255, nullable: true)] + protected ?string $token = null; + + /** + * Constructor. */ - public function saveParams($params) + public function __construct() { - ksort($params); - $processedParams = []; - foreach ($params as $key => $value) { - $processedParams[] = urlencode($key) . '=' . urlencode($value); - } - $this->params = implode('&', $processedParams); + // Set the default value as a DateTime object + $this->expires = $this->getUnassignedDefaultDateTime(); } /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int + public function getId(): ?int { - return $this->id; + return $this->id ?? null; } /** @@ -164,7 +159,7 @@ public function getToken(): ?string */ public function setExpiry(DateTime $dateTime): static { - $this->expires = $dateTime->format('Y-m-d H:i:s'); + $this->expires = $dateTime; return $this; } @@ -175,6 +170,6 @@ public function setExpiry(DateTime $dateTime): static */ public function getExpiry(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->expires); + return $this->expires; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/OaiResumptionEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/OaiResumptionEntityInterface.php index aaf88fbc29e..482e1d42ab3 100644 --- a/module/VuFind/src/VuFind/Db/Entity/OaiResumptionEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/OaiResumptionEntityInterface.php @@ -43,11 +43,11 @@ interface OaiResumptionEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Resumption parameters setter diff --git a/module/VuFind/src/VuFind/Db/Entity/PluginManager.php b/module/VuFind/src/VuFind/Db/Entity/PluginManager.php new file mode 100644 index 00000000000..f4db05e4afc --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/PluginManager.php @@ -0,0 +1,129 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use Laminas\ServiceManager\Factory\InvokableFactory; + +/** + * Database entity plugin manager + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager +{ + /** + * Default plugin aliases. + * + * @var array + */ + protected $aliases = [ + AccessTokenEntityInterface::class => AccessToken::class, + AuthHashEntityInterface::class => AuthHash::class, + ChangeTrackerEntityInterface::class => ChangeTracker::class, + CommentsEntityInterface::class => Comments::class, + ExternalSessionEntityInterface::class => ExternalSession::class, + FeedbackEntityInterface::class => Feedback::class, + LoginTokenEntityInterface::class => LoginToken::class, + OaiResumptionEntityInterface::class => OaiResumption::class, + RatingsEntityInterface::class => Ratings::class, + RecordEntityInterface::class => Record::class, + ResourceEntityInterface::class => Resource::class, + ResourceTagsEntityInterface::class => ResourceTags::class, + SearchEntityInterface::class => Search::class, + SessionEntityInterface::class => Session::class, + ShortlinksEntityInterface::class => Shortlinks::class, + TagsEntityInterface::class => Tags::class, + UserEntityInterface::class => User::class, + UserCardEntityInterface::class => UserCard::class, + UserListEntityInterface::class => UserList::class, + UserResourceEntityInterface::class => UserResource::class, + ]; + + /** + * Default plugin factories. + * + * @var array + */ + protected $factories = [ + AccessToken::class => InvokableFactory::class, + AuthHash::class => InvokableFactory::class, + ChangeTracker::class => InvokableFactory::class, + Comments::class => InvokableFactory::class, + ExternalSession::class => InvokableFactory::class, + Feedback::class => InvokableFactory::class, + LoginToken::class => InvokableFactory::class, + OaiResumption::class => InvokableFactory::class, + Ratings::class => InvokableFactory::class, + Record::class => InvokableFactory::class, + Resource::class => InvokableFactory::class, + ResourceTags::class => InvokableFactory::class, + Search::class => InvokableFactory::class, + Session::class => InvokableFactory::class, + Shortlinks::class => InvokableFactory::class, + Tags::class => InvokableFactory::class, + User::class => InvokableFactory::class, + UserCard::class => InvokableFactory::class, + UserList::class => InvokableFactory::class, + UserResource::class => InvokableFactory::class, + ]; + + /** + * We do not want to create shared instances of database entities; build a new + * one every time! + * + * @var bool + */ + protected $sharedByDefault = false; + + /** + * Return the name of the base class or interface that plug-ins must conform + * to. + * + * @return string + */ + protected function getExpectedInterface() + { + return EntityInterface::class; + } + + /** + * Get aliases. + * + * @return array + */ + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/Ratings.php b/module/VuFind/src/VuFind/Db/Entity/Ratings.php similarity index 50% rename from module/VuFind/src/VuFind/Db/Row/Ratings.php rename to module/VuFind/src/VuFind/Db/Entity/Ratings.php index a4ded638a5d..232dd9c4bdc 100644 --- a/module/VuFind/src/VuFind/Db/Row/Ratings.php +++ b/module/VuFind/src/VuFind/Db/Entity/Ratings.php @@ -1,11 +1,11 @@ + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\RatingsEntityInterface; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; -use VuFind\Db\Service\UserServiceInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for ratings + * Entity model for ratings table * * @category VuFind - * @package Db_Row - * @author Ere Maijala + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property int $resource_id - * @property int $rating - * @property string $created + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class Ratings extends RowGateway implements - RatingsEntityInterface, - \VuFind\Db\Table\DbTableAwareInterface, - DbServiceAwareInterface +#[ORM\Table(name: 'ratings')] +#[ORM\Index(name: 'ratings_user_id_idx', columns: ['user_id'])] +#[ORM\Index(name: 'ratings_resource_id_idx', columns: ['resource_id'])] +#[ORM\Entity] +class Ratings implements RatingsEntityInterface { - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * User ID. + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * Resource ID. + * + * @var ResourceEntityInterface + */ + #[ORM\JoinColumn( + name: 'resource_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: ['default' => 0] + )] + #[ORM\ManyToOne(targetEntity: ResourceEntityInterface::class)] + protected ResourceEntityInterface $resource; + + /** + * Rating. + * + * @var int + */ + #[ORM\Column(name: 'rating', type: 'integer', nullable: false)] + protected int $rating; /** - * Constructor + * Creation date. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Constructor. */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'ratings', $adapter); + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); } /** @@ -78,19 +116,17 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** * Get user. * - * @return ?UserEntityInterface + * @return ?UserEntityInterface; */ public function getUser(): ?UserEntityInterface { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; + return $this->user; } /** @@ -102,7 +138,7 @@ public function getUser(): ?UserEntityInterface */ public function setUser(?UserEntityInterface $user): static { - $this->user_id = $user?->getId(); + $this->user = $user; return $this; } @@ -113,9 +149,7 @@ public function setUser(?UserEntityInterface $user): static */ public function getResource(): ResourceEntityInterface { - return $this->resource_id - ? $this->getDbServiceManager()->get(ResourceServiceInterface::class)->getResourceById($this->resource_id) - : null; + return $this->resource; } /** @@ -127,7 +161,7 @@ public function getResource(): ResourceEntityInterface */ public function setResource(ResourceEntityInterface $resource): static { - $this->resource_id = $resource->getId(); + $this->resource = $resource; return $this; } @@ -138,7 +172,7 @@ public function setResource(ResourceEntityInterface $resource): static */ public function getRating(): int { - return $this->rating ?? ''; + return $this->rating; } /** @@ -161,7 +195,7 @@ public function setRating(int $rating): static */ public function getCreated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); + return $this->created; } /** @@ -173,7 +207,7 @@ public function getCreated(): DateTime */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Row/Record.php b/module/VuFind/src/VuFind/Db/Entity/Record.php similarity index 52% rename from module/VuFind/src/VuFind/Db/Row/Record.php rename to module/VuFind/src/VuFind/Db/Entity/Record.php index 1e3b897d0ae..1f5e00f86c0 100644 --- a/module/VuFind/src/VuFind/Db/Row/Record.php +++ b/module/VuFind/src/VuFind/Db/Entity/Record.php @@ -1,11 +1,11 @@ + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use Exception; -use VuFind\Db\Entity\RecordEntityInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for user + * Record * * @category VuFind - * @package Db_Row - * @author Markus Beh + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $record_id - * @property string $source - * @property string $version - * @property string $updated + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class Record extends RowGateway implements RecordEntityInterface +#[ORM\Table(name: 'record')] +#[ORM\UniqueConstraint( + name: 'record_record_id_source_index', + columns: ['record_id', 'source'], + options: ['lengths' => [140, null]] +)] +#[ORM\Entity] +class Record implements RecordEntityInterface { + use DateTimeTrait; + /** - * Constructor + * Unique ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Record ID. + * + * @var ?string + */ + #[ORM\Column(name: 'record_id', type: 'string', length: 255, nullable: true)] + protected ?string $recordId = null; + + /** + * Record source. + * + * @var ?string + */ + #[ORM\Column(name: 'source', type: 'string', length: 50, nullable: true)] + protected ?string $source = null; + + /** + * Record version. + * + * @var string + */ + #[ORM\Column(name: 'version', type: 'string', length: 20, nullable: false)] + protected string $version; + + /** + * Record Data. + * + * @var ?string + */ + #[ORM\Column(name: 'data', type: 'text', length: 0, nullable: true)] + protected ?string $data = null; + + /** + * Updated date. + * + * @var DateTime + */ + #[ORM\Column(name: 'updated', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $updated; + + /** + * Constructor. */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'record', $adapter); + // Set the default value as a DateTime object + $this->updated = $this->getUnassignedDefaultDateTime(); } /** @@ -67,7 +119,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -77,7 +129,7 @@ public function getId(): ?int */ public function getRecordId(): ?string { - return $this->record_id ?? null; + return $this->recordId; } /** @@ -89,7 +141,7 @@ public function getRecordId(): ?string */ public function setRecordId(?string $recordId): static { - $this->record_id = $recordId; + $this->recordId = $recordId; return $this; } @@ -100,19 +152,19 @@ public function setRecordId(?string $recordId): static */ public function getSource(): ?string { - return $this->source ?? null; + return $this->source; } /** * Set record source. * - * @param ?string $recordSource Record source + * @param ?string $source Record source * * @return static */ - public function setSource(?string $recordSource): static + public function setSource(?string $source): static { - $this->source = $recordSource; + $this->source = $source; return $this; } @@ -123,7 +175,7 @@ public function setSource(?string $recordSource): static */ public function getVersion(): string { - return $this->version ?? ''; + return $this->version; } /** @@ -146,11 +198,7 @@ public function setVersion(string $recordVersion): static */ public function getData(): ?string { - try { - return $this->__get('data'); - } catch (Exception) { - return null; - } + return $this->data; } /** @@ -162,7 +210,7 @@ public function getData(): ?string */ public function setData(?string $recordData): static { - $this->__set('data', $recordData); + $this->data = $recordData; return $this; } @@ -173,7 +221,7 @@ public function setData(?string $recordData): static */ public function getUpdated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->updated); + return $this->updated; } /** @@ -185,7 +233,7 @@ public function getUpdated(): DateTime */ public function setUpdated(DateTime $dateTime): static { - $this->updated = $dateTime->format('Y-m-d H:i:s'); + $this->updated = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/Resource.php b/module/VuFind/src/VuFind/Db/Entity/Resource.php new file mode 100644 index 00000000000..3f6f2da6414 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Resource.php @@ -0,0 +1,233 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use Doctrine\ORM\Mapping as ORM; + +/** + * Resource + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'resource')] +#[ORM\Index(name: 'resource_record_id_idx', columns: ['record_id'], options: ['lengths' => [190]])] +#[ORM\Entity] +class Resource implements ResourceEntityInterface +{ + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Record ID. + * + * @var string + */ + #[ORM\Column(name: 'record_id', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $recordId = ''; + + /** + * Record title. + * + * @var string + */ + #[ORM\Column(name: 'title', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $title = ''; + + /** + * Primary author. + * + * @var ?string + */ + #[ORM\Column(name: 'author', type: 'string', length: 255, nullable: true)] + protected ?string $author = null; + + /** + * Published year. + * + * @var ?int + */ + #[ORM\Column(name: 'year', type: 'integer', nullable: true)] + protected ?int $year = null; + + /** + * Record source. + * + * @var string + */ + #[ORM\Column(name: 'source', type: 'string', length: 50, nullable: false, options: ['default' => 'Solr'])] + protected string $source = 'Solr'; + + /** + * Record Metadata + * + * @var ?string + */ + #[ORM\Column(name: 'extra_metadata', type: 'text', length: 16777215, nullable: true)] + protected ?string $extraMetadata = null; + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Record Id setter + * + * @param string $recordId recordId + * + * @return static + */ + public function setRecordId(string $recordId): static + { + $this->recordId = $recordId; + return $this; + } + + /** + * Record Id getter + * + * @return string + */ + public function getRecordId(): string + { + return $this->recordId; + } + + /** + * Title setter + * + * @param string $title Title of the record. + * + * @return static + */ + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + /** + * Title getter + * + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * Author setter + * + * @param ?string $author Author of the title. + * + * @return static + */ + public function setAuthor(?string $author): static + { + $this->author = $author; + return $this; + } + + /** + * Year setter + * + * @param ?int $year Year title is published. + * + * @return static + */ + public function setYear(?int $year): static + { + $this->year = $year; + return $this; + } + + /** + * Source setter + * + * @param string $source Source (a search backend ID). + * + * @return static + */ + public function setSource(string $source): static + { + $this->source = $source; + return $this; + } + + /** + * Source getter + * + * @return string + */ + public function getSource(): string + { + return $this->source; + } + + /** + * Extra Metadata setter + * + * @param ?string $extraMetadata ExtraMetadata. + * + * @return static + */ + public function setExtraMetadata(?string $extraMetadata): static + { + $this->extraMetadata = $extraMetadata; + return $this; + } + + /** + * Extra Metadata getter + * + * @return ?string + */ + public function getExtraMetadata(): ?string + { + return $this->extraMetadata; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/ResourceEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/ResourceEntityInterface.php index bf7dbae1e85..9a034714dba 100644 --- a/module/VuFind/src/VuFind/Db/Entity/ResourceEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/ResourceEntityInterface.php @@ -41,11 +41,11 @@ interface ResourceEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Record Id setter diff --git a/module/VuFind/src/VuFind/Db/Row/ResourceTags.php b/module/VuFind/src/VuFind/Db/Entity/ResourceTags.php similarity index 51% rename from module/VuFind/src/VuFind/Db/Row/ResourceTags.php rename to module/VuFind/src/VuFind/Db/Entity/ResourceTags.php index 783a212f8a7..a626154ac78 100644 --- a/module/VuFind/src/VuFind/Db/Row/ResourceTags.php +++ b/module/VuFind/src/VuFind/Db/Entity/ResourceTags.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\ResourceTagsEntityInterface; -use VuFind\Db\Entity\TagsEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; -use VuFind\Db\Service\TagServiceInterface; -use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Db\Service\UserServiceInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for resource_tags + * ResourceTags * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $resource_id - * @property int $tag_id - * @property int $list_id - * @property int $user_id - * @property string $posted + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class ResourceTags extends RowGateway implements - ResourceTagsEntityInterface, - \VuFind\Db\Table\DbTableAwareInterface, - DbServiceAwareInterface +#[ORM\Table(name: 'resource_tags')] +#[ORM\Index(name: 'resource_tags_user_id_idx', columns: ['user_id'])] +#[ORM\Index(name: 'resource_tags_resource_id_idx', columns: ['resource_id'])] +#[ORM\Index(name: 'resource_tags_tag_id_idx', columns: ['tag_id'])] +#[ORM\Index(name: 'resource_tags_list_id_idx', columns: ['list_id'])] +#[ORM\Entity] +class ResourceTags implements ResourceTagsEntityInterface { - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; /** - * Constructor + * Posted time. + * + * @var DateTime + */ + #[ORM\Column(name: 'posted', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $posted; + + /** + * Resource ID. + * + * @var ?ResourceEntityInterface + */ + #[ORM\JoinColumn(name: 'resource_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: ResourceEntityInterface::class)] + protected ?ResourceEntityInterface $resource = null; + + /** + * Tag ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var TagsEntityInterface + */ + #[ORM\JoinColumn( + name: 'tag_id', + referencedColumnName: 'id', + nullable: false, + onDelete: 'CASCADE', + options: ['default' => 0] + )] + #[ORM\ManyToOne(targetEntity: TagsEntityInterface::class)] + protected TagsEntityInterface $tag; + + /** + * List ID. + * + * @var ?UserListEntityInterface + */ + #[ORM\JoinColumn(name: 'list_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserListEntityInterface::class)] + protected ?UserListEntityInterface $list = null; + + /** + * User ID. + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * Constructor */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'resource_tags', $adapter); + // Set the default value as a \DateTime object + $this->posted = new DateTime(); } /** @@ -83,7 +125,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -93,9 +135,7 @@ public function getId(): ?int */ public function getResource(): ?ResourceEntityInterface { - return $this->resource_id - ? $this->getDbServiceManager()->get(ResourceServiceInterface::class)->getResourceById($this->resource_id) - : null; + return $this->resource; } /** @@ -107,7 +147,7 @@ public function getResource(): ?ResourceEntityInterface */ public function setResource(?ResourceEntityInterface $resource): static { - $this->resource_id = $resource?->getId(); + $this->resource = $resource; return $this; } @@ -118,9 +158,7 @@ public function setResource(?ResourceEntityInterface $resource): static */ public function getTag(): TagsEntityInterface { - return $this->tag_id - ? $this->getDbServiceManager()->get(TagServiceInterface::class)->getTagById($this->tag_id) - : null; + return $this->tag; } /** @@ -132,7 +170,7 @@ public function getTag(): TagsEntityInterface */ public function setTag(TagsEntityInterface $tag): static { - $this->tag_id = $tag->getId(); + $this->tag = $tag; return $this; } @@ -143,9 +181,7 @@ public function setTag(TagsEntityInterface $tag): static */ public function getUserList(): ?UserListEntityInterface { - return $this->list_id - ? $this->getDbServiceManager()->get(UserListServiceInterface::class)->getUserListById($this->list_id) - : null; + return $this->list; } /** @@ -157,7 +193,7 @@ public function getUserList(): ?UserListEntityInterface */ public function setUserList(?UserListEntityInterface $list): static { - $this->list_id = $list?->getId(); + $this->list = $list; return $this; } @@ -168,21 +204,19 @@ public function setUserList(?UserListEntityInterface $list): static */ public function getUser(): ?UserEntityInterface { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; + return $this->user; } /** * Set user. * - * @param ?UserEntityInterface $user User + * @param ?UserEntityInterface $user User object * * @return static */ public function setUser(?UserEntityInterface $user): static { - $this->user_id = $user?->getId(); + $this->user = $user; return $this; } @@ -193,7 +227,7 @@ public function setUser(?UserEntityInterface $user): static */ public function getPosted(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->posted); + return $this->posted; } /** @@ -205,7 +239,7 @@ public function getPosted(): DateTime */ public function setPosted(DateTime $dateTime): static { - $this->posted = $dateTime->format('Y-m-d H:i:s'); + $this->posted = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/Search.php b/module/VuFind/src/VuFind/Db/Entity/Search.php new file mode 100644 index 00000000000..07b5e90b0c7 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Search.php @@ -0,0 +1,451 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; +use VuFind\Search\Minified; + +use function is_object; +use function is_resource; + +/** + * Search + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'search')] +#[ORM\Index(name: 'notification_base_url_idx', columns: ['notification_base_url'], options: ['lengths' => [190]])] +#[ORM\Index(name: 'notification_frequency_idx', columns: ['notification_frequency'])] +#[ORM\Index(name: 'search_created_saved_idx', columns: ['created', 'saved'])] +#[ORM\Index(name: 'session_id_idx', columns: ['session_id'])] +#[ORM\Index(name: 'search_user_id_idx', columns: ['user_id'])] +#[ORM\Entity] +class Search implements SearchEntityInterface +{ + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'bigint', nullable: false, options: ['unsigned' => true])] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * User ID. + * + * @var ?UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected ?UserEntityInterface $user = null; + + /** + * Session ID. + * + * @var ?string + */ + #[ORM\Column(name: 'session_id', type: 'string', length: 128, nullable: true)] + protected ?string $sessionId = null; + + /** + * Created date. + * + * @var DateTime + */ + #[ORM\Column( + name: 'created', + type: 'datetime', + nullable: false, + options: ['default' => '2000-01-01 00:00:00'] + )] + protected DateTime $created; + + /** + * Title. + * + * @var ?string + */ + #[ORM\Column(name: 'title', type: 'string', length: 20, nullable: true)] + protected ?string $title = null; + + /** + * Saved. + * + * @var bool + */ + #[ORM\Column(name: 'saved', type: 'boolean', nullable: false, options: ['default' => false])] + protected bool $saved = false; + + /** + * Search object. + * + * @var mixed + */ + #[ORM\Column(name: 'search_object', type: 'blob', length: 65535, nullable: true)] + protected mixed $searchObject = null; + + /** + * Normalized search object after loading. + * + * @var ?Minified + */ + protected ?Minified $deserializedSearchObject = null; + + /** + * Checksum + * + * @var ?int + */ + #[ORM\Column(name: 'checksum', type: 'integer', nullable: true)] + protected ?int $checksum = null; + + /** + * Notification frequency. + * + * @var int + */ + #[ORM\Column(name: 'notification_frequency', type: 'integer', nullable: false, options: ['default' => 0])] + protected int $notificationFrequency = 0; + + /** + * Date last notification is sent. + * + * @var DateTime + */ + #[ORM\Column( + name: 'last_notification_sent', + type: 'datetime', + nullable: false, + options: ['default' => '2000-01-01 00:00:00'] + )] + protected DateTime $lastNotificationSent; + + /** + * Notification base URL. + * + * @var string + */ + #[ORM\Column( + name: 'notification_base_url', + type: 'string', + length: 255, + nullable: false, + options: ['default' => ''] + )] + protected string $notificationBaseUrl = ''; + + /** + * Constructor. + */ + public function __construct() + { + // Set the default values as DateTime objects + $this->created = $this->getUnassignedDefaultDateTime(); + $this->lastNotificationSent = $this->getUnassignedDefaultDateTime(); + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get user. + * + * @return ?UserEntityInterface + */ + public function getUser(): ?UserEntityInterface + { + return $this->user; + } + + /** + * Set user. + * + * @param ?UserEntityInterface $user User + * + * @return static + */ + public function setUser(?UserEntityInterface $user): static + { + $this->user = $user; + return $this; + } + + /** + * Get session identifier. + * + * @return ?string + */ + public function getSessionId(): ?string + { + return $this->sessionId; + } + + /** + * Set session identifier. + * + * @param ?string $sessionId Session id + * + * @return static + */ + public function setSessionId(?string $sessionId): static + { + $this->sessionId = $sessionId; + return $this; + } + + /** + * Get created date. + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Set created date. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Get title. + * + * @return ?string + */ + public function getTitle(): ?string + { + return $this->title; + } + + /** + * Set title. + * + * @param ?string $title Title + * + * @return static + */ + public function setTitle(?string $title): static + { + $this->title = $title; + return $this; + } + + /** + * Get saved. + * + * @return bool + */ + public function getSaved(): bool + { + return $this->saved; + } + + /** + * Set saved. + * + * @param bool $saved Saved + * + * @return static + */ + public function setSaved(bool $saved): static + { + $this->saved = $saved; + return $this; + } + + /** + * Post-load normalization (deserialization). + * + * @return void + */ + #[ORM\PostLoad] + public function postLoadNormalize(): void + { + // Only deserialize if searchObject is not null and not already deserialized + if ($this->searchObject && !is_object($this->deserializedSearchObject)) { + // If it's a resource (stream), convert it to a string first + if (is_resource($this->searchObject)) { + $this->searchObject = stream_get_contents($this->searchObject); + } + $unserialized = @unserialize($this->searchObject); + if ($unserialized && is_object($unserialized)) { + $this->deserializedSearchObject = $unserialized; + } else { + $this->deserializedSearchObject = null; + } + } + } + + /** + * Get the search object from the row. + * + * @return ?\VuFind\Search\Minified + */ + public function getSearchObject(): ?\VuFind\Search\Minified + { + // If the search object has not been resolved, do so now: + if (is_resource($this->searchObject)) { + $this->postLoadNormalize(); + } + return $this->deserializedSearchObject; + } + + /** + * Set search object. + * + * @param ?\VuFind\Search\Minified $searchObject Search object + * + * @return static + */ + public function setSearchObject(?\VuFind\Search\Minified $searchObject): static + { + $this->searchObject = $searchObject ? serialize($searchObject) : null; + $this->deserializedSearchObject = $searchObject; + return $this; + } + + /** + * Get checksum. + * + * @return ?int + */ + public function getChecksum(): ?int + { + return $this->checksum; + } + + /** + * Set checksum. + * + * @param ?int $checksum Checksum + * + * @return static + */ + public function setChecksum(?int $checksum): static + { + $this->checksum = $checksum; + return $this; + } + + /** + * Get notification frequency. + * + * @return int + */ + public function getNotificationFrequency(): int + { + return $this->notificationFrequency; + } + + /** + * Set notification frequency. + * + * @param int $notificationFrequency Notification frequency + * + * @return static + */ + public function setNotificationFrequency(int $notificationFrequency): static + { + $this->notificationFrequency = $notificationFrequency; + return $this; + } + + /** + * When was the last notification sent? + * + * @return DateTime + */ + public function getLastNotificationSent(): DateTime + { + return $this->lastNotificationSent; + } + + /** + * Set when last notification was sent. + * + * @param DateTime $lastNotificationSent Time when last notification was sent + * + * @return static + */ + public function setLastNotificationSent(Datetime $lastNotificationSent): static + { + $this->lastNotificationSent = $lastNotificationSent; + return $this; + } + + /** + * Get notification base URL. + * + * @return string + */ + public function getNotificationBaseUrl(): string + { + return $this->notificationBaseUrl; + } + + /** + * Set notification base URL. + * + * @param string $notificationBaseUrl Notification base URL + * + * @return static + */ + public function setNotificationBaseUrl(string $notificationBaseUrl): static + { + $this->notificationBaseUrl = $notificationBaseUrl; + return $this; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/Session.php b/module/VuFind/src/VuFind/Db/Entity/Session.php similarity index 51% rename from module/VuFind/src/VuFind/Db/Row/Session.php rename to module/VuFind/src/VuFind/Db/Entity/Session.php index 7db832b828a..a8f68ef2f10 100644 --- a/module/VuFind/src/VuFind/Db/Row/Session.php +++ b/module/VuFind/src/VuFind/Db/Entity/Session.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\SessionEntityInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for session + * Session * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property ?string $session_id - * @property string $data - * @property int $last_used - * @property string $created + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class Session extends RowGateway implements SessionEntityInterface +#[ORM\Table(name: 'session')] +#[ORM\Index(name: 'session_last_used_idx', columns: ['last_used'])] +#[ORM\UniqueConstraint(name: 'session_session_id_idx', columns: ['session_id'])] +#[ORM\Entity] +class Session implements SessionEntityInterface { + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'bigint', nullable: false, options: ['unsigned' => true])] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Session ID. + * + * @var ?string + */ + #[ORM\Column(name: 'session_id', type: 'string', length: 128, nullable: true)] + protected ?string $sessionId = null; + + /** + * Session data. + * + * @var ?string + */ + #[ORM\Column(name: 'data', type: 'text', length: 16777215, nullable: true)] + protected ?string $data = null; + /** - * Constructor + * Time session last used. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var int */ - public function __construct($adapter) + #[ORM\Column(name: 'last_used', type: 'integer', nullable: false, options: ['default' => 0])] + protected int $lastUsed = 0; + + /** + * Time session is created. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Constructor. + */ + public function __construct() { - parent::__construct('id', 'session', $adapter); + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); } /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int + public function getId(): ?int { - return $this->id; + return $this->id ?? null; } /** @@ -78,7 +120,7 @@ public function getId(): int */ public function setSessionId(?string $sid): static { - $this->session_id = $sid; + $this->sessionId = $sid; return $this; } @@ -91,7 +133,7 @@ public function setSessionId(?string $sid): static */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } @@ -104,7 +146,7 @@ public function setCreated(DateTime $dateTime): static */ public function setLastUsed(int $lastUsed): static { - $this->last_used = $lastUsed; + $this->lastUsed = $lastUsed; return $this; } @@ -115,7 +157,7 @@ public function setLastUsed(int $lastUsed): static */ public function getLastUsed(): int { - return $this->last_used; + return $this->lastUsed; } /** diff --git a/module/VuFind/src/VuFind/Db/Entity/SessionEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/SessionEntityInterface.php index d5065709c4d..4b560ea7b10 100644 --- a/module/VuFind/src/VuFind/Db/Entity/SessionEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/SessionEntityInterface.php @@ -43,11 +43,11 @@ interface SessionEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Session Id setter diff --git a/module/VuFind/src/VuFind/Db/Row/Shortlinks.php b/module/VuFind/src/VuFind/Db/Entity/Shortlinks.php similarity index 61% rename from module/VuFind/src/VuFind/Db/Row/Shortlinks.php rename to module/VuFind/src/VuFind/Db/Entity/Shortlinks.php index 13d1a716a24..0c0d6996063 100644 --- a/module/VuFind/src/VuFind/Db/Row/Shortlinks.php +++ b/module/VuFind/src/VuFind/Db/Entity/Shortlinks.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\ShortlinksEntityInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for shortlinks + * Shortlinks * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $path - * @property string $hash - * @property string $created + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class Shortlinks extends RowGateway implements ShortlinksEntityInterface +#[ORM\Table(name: 'shortlinks')] +#[ORM\UniqueConstraint(name: 'shortlinks_hash_IDX', columns: ['hash'])] +#[ORM\Entity] +class Shortlinks implements ShortlinksEntityInterface { /** - * Constructor + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Path (minus hostname) from shortened URL. + * + * @var string + */ + #[ORM\Column(name: 'path', type: 'text', length: 16777215, nullable: false)] + protected string $path; + + /** + * Shortlinks hash. + * + * @var ?string + */ + #[ORM\Column(name: 'hash', type: 'string', length: 32, nullable: true)] + protected ?string $hash = null; + + /** + * Creation timestamp. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $created; + + /** + * Constructor */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'shortlinks', $adapter); + // Set the default value as a \DateTime object + $this->created = new DateTime(); } /** @@ -75,7 +106,7 @@ public function getId(): ?int */ public function getPath(): string { - return $this->path ?? ''; + return $this->path; } /** @@ -99,7 +130,7 @@ public function setPath(string $path): static */ public function getHash(): ?string { - return $this->hash ?? null; + return $this->hash; } /** @@ -122,7 +153,7 @@ public function setHash(?string $hash): static */ public function getCreated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); + return $this->created; } /** @@ -134,7 +165,7 @@ public function getCreated(): DateTime */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/Tags.php b/module/VuFind/src/VuFind/Db/Entity/Tags.php new file mode 100644 index 00000000000..763d3e5cc19 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Tags.php @@ -0,0 +1,97 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use Doctrine\ORM\Mapping as ORM; + +/** + * Tags + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'tags')] +#[ORM\Entity] +class Tags implements TagsEntityInterface +{ + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Name of tag. + * + * @var string + */ + #[ORM\Column(name: 'tag', type: 'string', length: 64, nullable: false, options: ['default' => ''])] + protected string $tag = ''; + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Tag setter + * + * @param string $tag Tag + * + * @return static + */ + public function setTag(string $tag): static + { + $this->tag = $tag; + return $this; + } + + /** + * Tag getter + * + * @return string + */ + public function getTag(): string + { + return $this->tag; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/TagsEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/TagsEntityInterface.php index 430096035d5..d60a49865dd 100644 --- a/module/VuFind/src/VuFind/Db/Entity/TagsEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/TagsEntityInterface.php @@ -41,11 +41,11 @@ interface TagsEntityInterface extends EntityInterface { /** - * Id getter + * Get identifier (returns null for an uninitialized or non-persisted object). * - * @return int + * @return ?int */ - public function getId(): int; + public function getId(): ?int; /** * Tag setter diff --git a/module/VuFind/src/VuFind/Db/Entity/User.php b/module/VuFind/src/VuFind/Db/Entity/User.php new file mode 100644 index 00000000000..255244a47ad --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/User.php @@ -0,0 +1,746 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; + +/** + * User + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: '`user`')] +#[ORM\UniqueConstraint(name: 'user_cat_id_idx', columns: ['cat_id'], options: ['lengths' => [190]])] +#[ORM\UniqueConstraint(name: 'user_username_idx', columns: ['username'], options: ['lengths' => [190]])] +#[ORM\Index(name: 'user_email_idx', columns: ['email'], options: ['lengths' => [190]])] +#[ORM\Index(name: 'user_verify_hash_idx', columns: ['verify_hash'])] +#[ORM\Entity] +class User implements UserEntityInterface +{ + use DateTimeTrait; + use ExchangeArrayTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Username + * + * @var string + */ + #[ORM\Column(name: 'username', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $username = ''; + + /** + * Password + * + * @var string + */ + #[ORM\Column(name: 'password', type: 'string', length: 32, nullable: false, options: ['default' => ''])] + protected string $password = ''; + + /** + * Hash of the password. + * + * @var ?string + */ + #[ORM\Column(name: 'pass_hash', type: 'string', length: 60, nullable: true)] + protected ?string $passHash = null; + + /** + * First Name. + * + * @var string + */ + #[ORM\Column(name: 'firstname', type: 'string', length: 50, nullable: false, options: ['default' => ''])] + protected string $firstname = ''; + + /** + * Last Name. + * + * @var string + */ + #[ORM\Column(name: 'lastname', type: 'string', length: 50, nullable: false, options: ['default' => ''])] + protected string $lastname = ''; + + /** + * Email. + * + * @var string + */ + #[ORM\Column(name: 'email', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $email = ''; + + /** + * Date of email verification. + * + * @var ?DateTime + */ + #[ORM\Column(name: 'email_verified', type: 'datetime', nullable: true)] + protected ?DateTime $emailVerified = null; + + /** + * Pending email. + * + * @var string + */ + #[ORM\Column(name: 'pending_email', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $pendingEmail = ''; + + /** + * User provided email. + * + * @var bool + */ + #[ORM\Column(name: 'user_provided_email', type: 'boolean', nullable: false, options: ['default' => false])] + protected bool $userProvidedEmail = false; + + /** + * Cat ID. + * + * @var ?string + */ + #[ORM\Column(name: 'cat_id', type: 'string', length: 255, nullable: true)] + protected ?string $catId = null; + + /** + * Cat username. + * + * @var ?string + */ + #[ORM\Column(name: 'cat_username', type: 'string', length: 50, nullable: true)] + protected ?string $catUsername = null; + + /** + * Cat password. + * + * @var ?string + */ + #[ORM\Column(name: 'cat_password', type: 'string', length: 70, nullable: true)] + protected ?string $catPassword = null; + + /** + * Cat encrypted password. + * + * @var ?string + */ + #[ORM\Column(name: 'cat_pass_enc', type: 'string', length: 255, nullable: true)] + protected ?string $catPassEnc = null; + + /** + * College. + * + * @var string + */ + #[ORM\Column(name: 'college', type: 'string', length: 100, nullable: false, options: ['default' => ''])] + protected string $college = ''; + + /** + * Major. + * + * @var string + */ + #[ORM\Column(name: 'major', type: 'string', length: 100, nullable: false, options: ['default' => ''])] + protected string $major = ''; + + /** + * Home library. + * + * @var ?string + */ + #[ORM\Column(name: 'home_library', type: 'string', length: 100, nullable: true, options: ['default' => ''])] + protected ?string $homeLibrary = ''; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Verify hash. + * + * @var string + */ + #[ORM\Column(name: 'verify_hash', type: 'string', length: 42, nullable: false, options: ['default' => ''])] + protected string $verifyHash = ''; + + /** + * Time last loggedin. + * + * @var DateTime + */ + #[ORM\Column(name: 'last_login', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $lastLogin; + + /** + * Method of authentication. + * + * @var ?string + */ + #[ORM\Column(name: 'auth_method', type: 'string', length: 50, nullable: true)] + protected ?string $authMethod = null; + + /** + * Last known language. + * + * @var string + */ + #[ORM\Column(name: 'last_language', type: 'string', length: 30, nullable: false, options: ['default' => ''])] + protected string $lastLanguage = ''; + + /** + * Constructor + */ + public function __construct() + { + // Set the default values as DateTime objects + $this->created = $this->getUnassignedDefaultDateTime(); + $this->lastLogin = $this->getUnassignedDefaultDateTime(); + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Username setter + * + * @param string $username Username + * + * @return static + */ + public function setUsername(string $username): static + { + $this->username = $username; + return $this; + } + + /** + * Get username. + * + * @return string + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * Set raw (unhashed) password (if available). This should only be used when hashing is disabled. + * + * @param string $password Password + * + * @return static + */ + public function setRawPassword(string $password): static + { + $this->password = $password; + return $this; + } + + /** + * Get raw (unhashed) password (if available). This should only be used when hashing is disabled. + * + * @return string + */ + public function getRawPassword(): string + { + return $this->password ?? ''; + } + + /** + * Set hashed password. This should only be used when hashing is enabled. + * + * @param ?string $hash Password hash + * + * @return static + */ + public function setPasswordHash(?string $hash): static + { + $this->passHash = $hash; + return $this; + } + + /** + * Get hashed password. This should only be used when hashing is enabled. + * + * @return ?string + */ + public function getPasswordHash(): ?string + { + return $this->passHash; + } + + /** + * Set firstname. + * + * @param string $firstName New first name + * + * @return static + */ + public function setFirstname(string $firstName): static + { + $this->firstname = $firstName; + return $this; + } + + /** + * Get firstname. + * + * @return string + */ + public function getFirstname(): string + { + return $this->firstname; + } + + /** + * Set lastname. + * + * @param string $lastName New last name + * + * @return static + */ + public function setLastname(string $lastName): static + { + $this->lastname = $lastName; + return $this; + } + + /** + * Get lastname. + * + * @return string + */ + public function getLastname(): string + { + return $this->lastname; + } + + /** + * Set email. + * + * @param string $email Email address + * + * @return static + */ + public function setEmail(string $email): static + { + $this->email = $email; + return $this; + } + + /** + * Get email. + * + * @return string + */ + public function getEmail(): string + { + return $this->email; + } + + /** + * Set pending email. + * + * @param string $email New pending email + * + * @return static + */ + public function setPendingEmail(string $email): static + { + $this->pendingEmail = $email; + return $this; + } + + /** + * Get pending email. + * + * @return string + */ + public function getPendingEmail(): string + { + return $this->pendingEmail; + } + + /** + * Catalog id setter + * + * @param ?string $catId Catalog id + * + * @return static + */ + public function setCatId(?string $catId): static + { + $this->catId = $catId; + return $this; + } + + /** + * Get catalog id. + * + * @return ?string + */ + public function getCatId(): ?string + { + return $this->catId; + } + + /** + * Catalog username setter + * + * @param ?string $catUsername Catalog username + * + * @return static + */ + public function setCatUsername(?string $catUsername): static + { + $this->catUsername = $catUsername; + return $this; + } + + /** + * Get catalog username. + * + * @return ?string + */ + public function getCatUsername(): ?string + { + return $this->catUsername; + } + + /** + * Home library setter + * + * @param ?string $homeLibrary Home library + * + * @return static + */ + public function setHomeLibrary(?string $homeLibrary): static + { + $this->homeLibrary = $homeLibrary; + return $this; + } + + /** + * Get home library. + * + * @return ?string + */ + public function getHomeLibrary(): ?string + { + return $this->homeLibrary; + } + + /** + * Raw catalog password setter + * + * @param ?string $catPassword Cat password + * + * @return static + */ + public function setRawCatPassword(?string $catPassword): static + { + $this->catPassword = $catPassword; + return $this; + } + + /** + * Get raw catalog password. + * + * @return ?string + */ + public function getRawCatPassword(): ?string + { + return $this->catPassword; + } + + /** + * Encrypted catalog password setter + * + * @param ?string $passEnc Encrypted password + * + * @return static + */ + public function setCatPassEnc(?string $passEnc): static + { + $this->catPassEnc = $passEnc; + return $this; + } + + /** + * Get encrypted catalog password. + * + * @return ?string + */ + public function getCatPassEnc(): ?string + { + return $this->catPassEnc; + } + + /** + * Set college. + * + * @param string $college College + * + * @return static + */ + public function setCollege(string $college): static + { + $this->college = $college; + return $this; + } + + /** + * Get college. + * + * @return string + */ + public function getCollege(): string + { + return $this->college; + } + + /** + * Set major. + * + * @param string $major Major + * + * @return static + */ + public function setMajor(string $major): static + { + $this->major = $major; + return $this; + } + + /** + * Get major. + * + * @return string + */ + public function getMajor(): string + { + return $this->major; + } + + /** + * Set verification hash for recovery. + * + * @param string $hash Hash value to save + * + * @return static + */ + public function setVerifyHash(string $hash): static + { + $this->verifyHash = $hash; + return $this; + } + + /** + * Get verification hash for recovery. + * + * @return string + */ + public function getVerifyHash(): string + { + return $this->verifyHash; + } + + /** + * Set active authentication method (if any). + * + * @param ?string $authMethod New value (null for none) + * + * @return static + */ + public function setAuthMethod(?string $authMethod): static + { + $this->authMethod = $authMethod; + return $this; + } + + /** + * Get active authentication method (if any). + * + * @return ?string + */ + public function getAuthMethod(): ?string + { + return $this->authMethod; + } + + /** + * Set last language. + * + * @param string $lang Last language + * + * @return static + */ + public function setLastLanguage(string $lang): static + { + $this->lastLanguage = $lang; + return $this; + } + + /** + * Get last language. + * + * @return string + */ + public function getLastLanguage(): string + { + return $this->lastLanguage; + } + + /** + * Does the user have a user-provided (true) vs. automatically looked up (false) email address? + * + * @return bool + */ + public function hasUserProvidedEmail(): bool + { + return $this->userProvidedEmail; + } + + /** + * Set the flag indicating whether the email address is user-provided. + * + * @param bool $userProvided New value + * + * @return static + */ + public function setHasUserProvidedEmail(bool $userProvided): static + { + $this->userProvidedEmail = $userProvided; + return $this; + } + + /** + * Last login setter. + * + * @param ?DateTime $dateTime Last login date + * + * @return static + */ + public function setLastLogin(?DateTime $dateTime): static + { + $this->lastLogin = $this->getNonNullableDateTimeFromNullable($dateTime); + return $this; + } + + /** + * Last login getter + * + * @return ?DateTime + */ + public function getLastLogin(): ?DateTime + { + return $this->getNullableDateTimeFromNonNullable($this->lastLogin); + } + + /** + * Created setter + * + * @param DateTime $dateTime Last login date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Created getter + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Set email verification date (or null for unverified). + * + * @param ?DateTime $dateTime Verification date (or null) + * + * @return static + */ + public function setEmailVerified(?DateTime $dateTime): static + { + $this->emailVerified = $dateTime; + return $this; + } + + /** + * Get email verification date (or null for unverified). + * + * @return ?DateTime + */ + public function getEmailVerified(): ?DateTime + { + return $this->emailVerified; + } + + /** + * Get the list of roles of this identity + * + * @return string[]|\Rbac\Role\RoleInterface[] + */ + public function getRoles() + { + return ['loggedin']; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/UserCard.php b/module/VuFind/src/VuFind/Db/Entity/UserCard.php similarity index 54% rename from module/VuFind/src/VuFind/Db/Row/UserCard.php rename to module/VuFind/src/VuFind/Db/Entity/UserCard.php index 0b9b14f8f72..75c52beffc3 100644 --- a/module/VuFind/src/VuFind/Db/Row/UserCard.php +++ b/module/VuFind/src/VuFind/Db/Entity/UserCard.php @@ -1,11 +1,11 @@ + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\UserCardEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; /** - * Row Definition for user_card + * UserCard * * @category VuFind - * @package Db_Row - * @author Ere Maijala + * @package Database + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property string $card_name - * @property string $cat_username - * @property ?string $cat_password - * @property ?string $cat_pass_enc - * @property ?string $home_library - * @property string $created - * @property string $saved + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class UserCard extends RowGateway implements DbServiceAwareInterface, UserCardEntityInterface +#[ORM\Table(name: 'user_card')] +#[ORM\Index(name: 'user_card_cat_username_idx', columns: ['cat_username'])] +#[ORM\Index(name: 'user_card_user_id_idx', columns: ['user_id'])] +#[ORM\Entity] +class UserCard implements UserCardEntityInterface { - use \VuFind\Db\Service\DbServiceAwareTrait; + use DateTimeTrait; /** - * Constructor + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Card name. + * + * @var string + */ + #[ORM\Column(name: 'card_name', type: 'string', length: 255, nullable: false, options: ['default' => ''])] + protected string $cardName = ''; + + /** + * Cat username. + * + * @var string + */ + #[ORM\Column(name: 'cat_username', type: 'string', length: 50, nullable: false, options: ['default' => ''])] + protected string $catUsername = ''; + + /** + * Cat password. + * + * @var ?string + */ + #[ORM\Column(name: 'cat_password', type: 'string', length: 70, nullable: true)] + protected ?string $catPassword = null; + + /** + * Cat password (encrypted). + * + * @var ?string + */ + #[ORM\Column(name: 'cat_pass_enc', type: 'string', length: 255, nullable: true)] + protected ?string $catPassEnc = null; + + /** + * Home library. + * + * @var ?string + */ + #[ORM\Column(name: 'home_library', type: 'string', length: 100, nullable: true, options: ['default' => ''])] + protected ?string $homeLibrary = ''; + + /** + * Creation date. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Saved timestamp. + * + * @var DateTime + */ + #[ORM\Column(name: 'saved', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $saved; + + /** + * User. + * + * @var UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected UserEntityInterface $user; + + /** + * Constructor */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'user_card', $adapter); + // Set the default value as a \DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); + $this->saved = new DateTime(); } /** @@ -86,7 +154,7 @@ public function getId(): ?int */ public function setCardName(string $cardName): static { - $this->card_name = $cardName; + $this->cardName = $cardName; return $this; } @@ -97,7 +165,7 @@ public function setCardName(string $cardName): static */ public function getCardName(): string { - return $this->card_name; + return $this->cardName; } /** @@ -109,7 +177,7 @@ public function getCardName(): string */ public function setCatUsername(string $catUsername): static { - $this->cat_username = $catUsername; + $this->catUsername = $catUsername; return $this; } @@ -120,7 +188,7 @@ public function setCatUsername(string $catUsername): static */ public function getCatUsername(): string { - return $this->cat_username; + return $this->catUsername; } /** @@ -132,7 +200,7 @@ public function getCatUsername(): string */ public function setRawCatPassword(?string $catPassword): static { - $this->cat_password = $catPassword; + $this->catPassword = $catPassword; return $this; } @@ -143,7 +211,7 @@ public function setRawCatPassword(?string $catPassword): static */ public function getRawCatPassword(): ?string { - return $this->cat_password; + return $this->catPassword; } /** @@ -155,7 +223,7 @@ public function getRawCatPassword(): ?string */ public function setCatPassEnc(?string $passEnc): static { - $this->cat_pass_enc = $passEnc; + $this->catPassEnc = $passEnc; return $this; } @@ -166,7 +234,7 @@ public function setCatPassEnc(?string $passEnc): static */ public function getCatPassEnc(): ?string { - return $this->cat_pass_enc; + return $this->catPassEnc; } /** @@ -178,7 +246,7 @@ public function getCatPassEnc(): ?string */ public function setHomeLibrary(?string $homeLibrary): static { - $this->home_library = $homeLibrary; + $this->homeLibrary = $homeLibrary; return $this; } @@ -189,7 +257,7 @@ public function setHomeLibrary(?string $homeLibrary): static */ public function getHomeLibrary(): ?string { - return $this->home_library; + return $this->homeLibrary; } /** @@ -201,7 +269,7 @@ public function getHomeLibrary(): ?string */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } @@ -212,7 +280,7 @@ public function setCreated(DateTime $dateTime): static */ public function getCreated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); + return $this->created; } /** @@ -224,7 +292,7 @@ public function getCreated(): DateTime */ public function setSaved(DateTime $dateTime): static { - $this->saved = $dateTime->format('Y-m-d H:i:s'); + $this->saved = $dateTime; return $this; } @@ -235,7 +303,7 @@ public function setSaved(DateTime $dateTime): static */ public function getSaved(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->saved); + return $this->saved; } /** @@ -247,7 +315,7 @@ public function getSaved(): DateTime */ public function setUser(UserEntityInterface $user): static { - $this->user_id = $user->getId(); + $this->user = $user; return $this; } @@ -258,6 +326,6 @@ public function setUser(UserEntityInterface $user): static */ public function getUser(): UserEntityInterface { - return $this->getDbService(\VuFind\Db\Service\UserServiceInterface::class)->getUserById($this->user_id); + return $this->user; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/UserEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/UserEntityInterface.php index 01f09830bae..5eeef71ae4f 100644 --- a/module/VuFind/src/VuFind/Db/Entity/UserEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/UserEntityInterface.php @@ -40,7 +40,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -interface UserEntityInterface extends EntityInterface, \LmcRbacMvc\Identity\IdentityInterface + +interface UserEntityInterface extends + EntityInterface, + ExchangeArrayInterface, + \LmcRbacMvc\Identity\IdentityInterface { /** * Get identifier (returns null for an uninitialized or non-persisted object). @@ -340,18 +344,18 @@ public function setHasUserProvidedEmail(bool $userProvided): static; /** * Last login setter. * - * @param DateTime $dateTime Last login date + * @param ?DateTime $dateTime Last login date * * @return static */ - public function setLastLogin(DateTime $dateTime): static; + public function setLastLogin(?DateTime $dateTime): static; /** * Last login getter * - * @return DateTime + * @return ?DateTime */ - public function getLastLogin(): DateTime; + public function getLastLogin(): ?DateTime; /** * Created setter diff --git a/module/VuFind/src/VuFind/Db/Entity/UserList.php b/module/VuFind/src/VuFind/Db/Entity/UserList.php new file mode 100644 index 00000000000..ad4ba011c59 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/UserList.php @@ -0,0 +1,236 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Entity; + +use DateTime; +use Doctrine\ORM\Mapping as ORM; +use VuFind\Db\Feature\DateTimeTrait; + +/** + * UserList + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +#[ORM\Table(name: 'user_list')] +#[ORM\Index(name: 'user_list_user_id_idx', columns: ['user_id'])] +#[ORM\Entity] +class UserList implements UserListEntityInterface +{ + use DateTimeTrait; + + /** + * Unique ID. + * + * @var int + */ + #[ORM\Id] + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; + + /** + * Title of the list. + * + * @var string + */ + #[ORM\Column(name: 'title', type: 'string', length: 200, nullable: false)] + protected string $title = ''; + + /** + * Description of the list. + * + * @var ?string + */ + #[ORM\Column(name: 'description', type: 'text', length: 65535, nullable: true)] + protected ?string $description = null; + + /** + * Creation date. + * + * @var DateTime + */ + #[ORM\Column(name: 'created', type: 'datetime', nullable: false, options: ['default' => '2000-01-01 00:00:00'])] + protected DateTime $created; + + /** + * Flag to indicate whether or not the list is public. + * + * @var bool + */ + #[ORM\Column(name: 'public', type: 'boolean', nullable: false, options: ['default' => false])] + protected bool $public = false; + + /** + * User ID. + * + * @var UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected UserEntityInterface $user; + + /** + * Constructor. + */ + public function __construct() + { + // Set the default value as a DateTime object + $this->created = $this->getUnassignedDefaultDateTime(); + } + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id ?? null; + } + + /** + * Set title. + * + * @param string $title Title + * + * @return static + */ + public function setTitle(string $title): static + { + $this->title = $title; + return $this; + } + + /** + * Get title. + * + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * Set description. + * + * @param ?string $description Description + * + * @return static + */ + public function setDescription(?string $description): static + { + $this->description = $description; + return $this; + } + + /** + * Get description. + * + * @return ?string + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Set created date. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Get created date. + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Set whether the list is public. + * + * @param bool $public Is the list public? + * + * @return static + */ + public function setPublic(bool $public): static + { + $this->public = $public; + return $this; + } + + /** + * Is this a public list? + * + * @return bool + */ + public function isPublic(): bool + { + return $this->public; + } + + /** + * Set user. + * + * @param UserEntityInterface $user User object + * + * @return static + */ + public function setUser(UserEntityInterface $user): static + { + $this->user = $user; + return $this; + } + + /** + * Get user. + * + * @return UserEntityInterface + */ + public function getUser(): UserEntityInterface + { + return $this->user; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/UserListEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/UserListEntityInterface.php index a78c5179677..1604364f37c 100644 --- a/module/VuFind/src/VuFind/Db/Entity/UserListEntityInterface.php +++ b/module/VuFind/src/VuFind/Db/Entity/UserListEntityInterface.php @@ -116,16 +116,16 @@ public function isPublic(): bool; /** * Set user. * - * @param ?UserEntityInterface $user User owning the list. + * @param UserEntityInterface $user User owning the list. * * @return static */ - public function setUser(?UserEntityInterface $user): static; + public function setUser(UserEntityInterface $user): static; /** * Get user. * - * @return ?UserEntityInterface + * @return UserEntityInterface */ - public function getUser(): ?UserEntityInterface; + public function getUser(): UserEntityInterface; } diff --git a/module/VuFind/src/VuFind/Db/Row/UserResource.php b/module/VuFind/src/VuFind/Db/Entity/UserResource.php similarity index 53% rename from module/VuFind/src/VuFind/Db/Row/UserResource.php rename to module/VuFind/src/VuFind/Db/Entity/UserResource.php index 85b9dd2d274..981b7d45f53 100644 --- a/module/VuFind/src/VuFind/Db/Row/UserResource.php +++ b/module/VuFind/src/VuFind/Db/Entity/UserResource.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; use DateTime; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Entity\UserResourceEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; -use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Db\Service\UserServiceInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for user_resource + * UserResource * * @category VuFind - * @package Db_Row + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property int $resource_id - * @property int $list_id - * @property string $notes - * @property string $saved + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class UserResource extends RowGateway implements - UserResourceEntityInterface, - \VuFind\Db\Table\DbTableAwareInterface, - DbServiceAwareInterface +#[ORM\Table(name: 'user_resource')] +#[ORM\Index(name: 'user_resource_list_id_idx', columns: ['list_id'])] +#[ORM\Index(name: 'user_resource_resource_id_idx', columns: ['resource_id'])] +#[ORM\Index(name: 'user_resource_user_id_idx', columns: ['user_id'])] +#[ORM\Entity] +class UserResource implements UserResourceEntityInterface { - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + */ + #[ORM\Column(name: 'id', type: 'integer', nullable: false)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + protected int $id; /** - * Constructor + * Notes associated with the resource. + * + * @var ?string + */ + #[ORM\Column(name: 'notes', type: 'text', length: 65535, nullable: true)] + protected ?string $notes = null; + + /** + * Date saved. + * + * @var DateTime + */ + #[ORM\Column(name: 'saved', type: 'datetime', nullable: false, options: ['default' => 'CURRENT_TIMESTAMP'])] + protected DateTime $saved; + + /** + * User ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var UserEntityInterface + */ + #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserEntityInterface::class)] + protected UserEntityInterface $user; + + /** + * Resource. + * + * @var ResourceEntityInterface + */ + #[ORM\JoinColumn(name: 'resource_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: ResourceEntityInterface::class)] + protected ResourceEntityInterface $resource; + + /** + * User list ID. + * + * @var ?UserListEntityInterface + */ + #[ORM\JoinColumn(name: 'list_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: UserListEntityInterface::class)] + protected ?UserListEntityInterface $list = null; + + /** + * Constructor */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'user_resource', $adapter); + // Set the default value as a \DateTime object + $this->saved = new DateTime(); } /** @@ -91,8 +127,7 @@ public function getId(): ?int */ public function getUser(): UserEntityInterface { - return $this->getDbServiceManager()->get(UserServiceInterface::class) - ->getUserById($this->user_id); + return $this->user; } /** @@ -104,7 +139,7 @@ public function getUser(): UserEntityInterface */ public function setUser(UserEntityInterface $user): static { - $this->user_id = $user->getId(); + $this->user = $user; return $this; } @@ -115,8 +150,7 @@ public function setUser(UserEntityInterface $user): static */ public function getResource(): ResourceEntityInterface { - return $this->getDbServiceManager()->get(ResourceServiceInterface::class) - ->getResourceById($this->resource_id); + return $this->resource; } /** @@ -128,7 +162,7 @@ public function getResource(): ResourceEntityInterface */ public function setResource(ResourceEntityInterface $resource): static { - $this->resource_id = $resource->getId(); + $this->resource = $resource; return $this; } @@ -139,21 +173,19 @@ public function setResource(ResourceEntityInterface $resource): static */ public function getUserList(): ?UserListEntityInterface { - return $this->list_id - ? $this->getDbServiceManager()->get(UserListServiceInterface::class)->getUserListById($this->list_id) - : null; + return $this->list; } /** * Set user list. * - * @param ?UserListEntityInterface $list User list + * @param ?UserListEntityInterface $list User List * * @return static */ public function setUserList(?UserListEntityInterface $list): static { - $this->list_id = $list?->getId(); + $this->list = $list; return $this; } @@ -164,7 +196,7 @@ public function setUserList(?UserListEntityInterface $list): static */ public function getNotes(): ?string { - return $this->notes ?? null; + return $this->notes; } /** @@ -187,7 +219,7 @@ public function setNotes(?string $notes): static */ public function getSaved(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->saved); + return $this->saved; } /** @@ -199,7 +231,7 @@ public function getSaved(): DateTime */ public function setSaved(DateTime $dateTime): static { - $this->saved = $dateTime->format('Y-m-d H:i:s'); + $this->saved = $dateTime; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/EntityManagerFactory.php b/module/VuFind/src/VuFind/Db/EntityManagerFactory.php new file mode 100644 index 00000000000..2ef4d61c176 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/EntityManagerFactory.php @@ -0,0 +1,101 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFind\Db; + +use Doctrine\ORM\EntityManager; +use DoctrineModule\Service\AbstractFactory; +use DoctrineORMModule\Options\EntityManager as DoctrineORMModuleEntityManager; +use Psr\Container\ContainerInterface; + +use function assert; + +/** + * Entity manager factory. + * + * Sets up the entity manager as described in the Doctrine ORM documentation + * so that targetEntity resolution will occur reliably. + * + * @category VuFind + * @package Db + * @author Aleksi Peebles + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class EntityManagerFactory extends AbstractFactory +{ + /** + * Create an object + * + * @param ContainerInterface $container Service manager + * @param string $requestedName Service being created + * @param null|array $options Extra options (optional) + * + * @return object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException&\Throwable if any other error occurs + */ + public function __invoke(ContainerInterface $container, $requestedName, array|null $options = null) + { + $options = $this->getOptions($container, 'entitymanager'); + assert($options instanceof DoctrineORMModuleEntityManager); + $connection = $container->get($options->getConnection()); + $config = $container->get($options->getConfiguration()); + + // Do not use the standard entity resolver and configuration since + // the mappings already exist in the plugin manager. + $pm = $container->get(\VuFind\Db\Entity\PluginManager::class); + + $evm = $connection->getEventManager(); + $rtel = new \Doctrine\ORM\Tools\ResolveTargetEntityListener(); + + foreach ($pm->getAliases() as $interface => $class) { + // Adds a target-entity class + $rtel->addResolveTargetEntity($interface, $class, []); + } + + // Add the ResolveTargetEntityListener + $evm->addEventSubscriber($rtel); + + return new EntityManager($connection, $config, $evm); + } + + /** + * Get the class name of the options associated with this factory. + * + * @return string + */ + public function getOptionsClass(): string + { + return DoctrineORMModuleEntityManager::class; + } +} diff --git a/module/VuFind/src/VuFind/Db/Feature/DateTimeTrait.php b/module/VuFind/src/VuFind/Db/Feature/DateTimeTrait.php new file mode 100644 index 00000000000..9896eee9209 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Feature/DateTimeTrait.php @@ -0,0 +1,82 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Feature; + +use DateTime; +use DateTimeZone; + +/** + * Trait providing date handling support functions. + * + * @category VuFind + * @package Database + * @author Ere Maijala + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +trait DateTimeTrait +{ + /** + * Get a date or null from non-nullable date with a default value. + * + * @param DateTime $date Date + * + * @return ?DateTime + */ + protected function getNullableDateTimeFromNonNullable(DateTime $date): ?DateTime + { + // Compare strings to avoid trouble with time zones: + return $date->format('Y-m-d H:i:s') !== $this->getUnassignedDefaultDateTime()->format('Y-m-d H:i:s') + ? $date + : null; + } + + /** + * Get a date or default value from nullable date. + * + * @param ?DateTime $date Date + * + * @return DateTime + */ + protected function getNonNullableDateTimeFromNullable(?DateTime $date): DateTime + { + return $date ?? $this->getUnassignedDefaultDateTime(); + } + + /** + * Get the value of default DateTime that has not been assigned a real date. + * + * @return DateTime + */ + protected function getUnassignedDefaultDateTime(): DateTime + { + return DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new DateTimeZone('UTC')); + } +} diff --git a/module/VuFind/src/VuFind/Db/PersistenceManager.php b/module/VuFind/src/VuFind/Db/PersistenceManager.php new file mode 100644 index 00000000000..55ac4cf50ed --- /dev/null +++ b/module/VuFind/src/VuFind/Db/PersistenceManager.php @@ -0,0 +1,113 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFind\Db; + +use Doctrine\ORM\EntityManagerInterface; +use VuFind\Auth\UserSessionPersistenceInterface; +use VuFind\Db\Entity\EntityInterface; +use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Service\DbServiceAwareInterface; +use VuFind\Db\Service\DbServiceAwareTrait; +use VuFind\Exception\DuplicateKeyException; + +/** + * Class to manage database persistence operations. + * + * @category VuFind + * @package Db + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ +class PersistenceManager implements DbServiceAwareInterface +{ + use DbServiceAwareTrait; + + /** + * Constructor + * + * @param EntityManagerInterface $entityManager Doctrine ORM entity manager + * @param bool $privacy Is user privacy mode enabled? + */ + public function __construct( + protected EntityManagerInterface $entityManager, + protected bool $privacy = false + ) { + } + + /** + * Persist an entity. + * + * @param EntityInterface $entity Entity to persist. + * + * @return void + */ + public function persistEntity(EntityInterface $entity): void + { + if ($this->privacy && $entity instanceof UserEntityInterface) { + $this->getDbService(UserSessionPersistenceInterface::class)->addUserDataToSession($entity); + return; + } + try { + $this->entityManager->persist($entity); + $this->entityManager->flush(); + } catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) { + throw $this->exceptionIndicatesDuplicateKey($e) + ? new DuplicateKeyException($e->getMessage(), $e->getCode(), $e) + : $e; + } + } + + /** + * Does the provided exception indicate that a duplicate key value has been + * created? + * + * @param \Exception $e Exception to check + * + * @return bool + */ + protected function exceptionIndicatesDuplicateKey(\Exception $e): bool + { + return strstr($e->getMessage(), 'Duplicate entry') !== false; + } + + /** + * Delete an entity. + * + * @param EntityInterface $entity Entity to persist. + * + * @return void + */ + public function deleteEntity(EntityInterface $entity): void + { + $this->entityManager->remove($entity); + $this->entityManager->flush(); + } +} diff --git a/module/VuFind/src/VuFind/Db/Table/CaseSensitiveTagsFactory.php b/module/VuFind/src/VuFind/Db/PersistenceManagerFactory.php similarity index 65% rename from module/VuFind/src/VuFind/Db/Table/CaseSensitiveTagsFactory.php rename to module/VuFind/src/VuFind/Db/PersistenceManagerFactory.php index f57f191bdce..99a403a4f85 100644 --- a/module/VuFind/src/VuFind/Db/Table/CaseSensitiveTagsFactory.php +++ b/module/VuFind/src/VuFind/Db/PersistenceManagerFactory.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki + * @link https://vufind.org Main Site */ -namespace VuFind\Db\Table; +namespace VuFind\Db; +use Doctrine\ORM\EntityManagerInterface; use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface as ContainerException; use Psr\Container\ContainerInterface; /** - * Shared Tags / ResourceTags table gateway factory. + * Factory for the persistence manager. * * @category VuFind - * @package Db_Table + * @package Database * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class CaseSensitiveTagsFactory extends GatewayFactory +class PersistenceManagerFactory implements FactoryInterface { /** * Create an object @@ -64,13 +66,23 @@ public function __invoke( $requestedName, ?array $options = null ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - $config = $container->get(\VuFind\Config\PluginManager::class) - ->get('config'); - $caseSensitive = isset($config->Social->case_sensitive_tags) - && $config->Social->case_sensitive_tags; - return parent::__invoke($container, $requestedName, [$caseSensitive]); + $config = $container->get(\VuFind\Config\PluginManager::class)->get('config'); + return new $requestedName( + $this->getEntityManager($container), + (bool)($config->Authentication->privacy ?? false), + ...($options ?? []) + ); + } + + /** + * Get entity manager to be passed to the constructor. + * + * @param ContainerInterface $container Service manager + * + * @return EntityManagerInterface + */ + protected function getEntityManager(ContainerInterface $container): EntityManagerInterface + { + return $container->get('doctrine.entitymanager.orm_vufind'); } } diff --git a/module/VuFind/src/VuFind/Db/Row/AccessToken.php b/module/VuFind/src/VuFind/Db/Row/AccessToken.php deleted file mode 100644 index f771064f8ba..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/AccessToken.php +++ /dev/null @@ -1,105 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use VuFind\Db\Entity\AccessTokenEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; - -/** - * Row Definition for access_token - * - * @category VuFind - * @package Db_Row - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class AccessToken extends RowGateway implements AccessTokenEntityInterface -{ - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct(['id', 'type'], 'access_token', $adapter); - } - - /** - * Set user ID. - * - * @param ?UserEntityInterface $user User owning token - * - * @return static - */ - public function setUser(?UserEntityInterface $user): static - { - $this->__set('user_id', $user?->getId()); - return $this; - } - - /** - * Set data. - * - * @param string $data Data - * - * @return static - */ - public function setData(string $data): static - { - $this->__set('data', $data); - return $this; - } - - /** - * Is the access token revoked? - * - * @return bool - */ - public function isRevoked(): bool - { - return $this->__get('revoked'); - } - - /** - * Set revoked status. - * - * @param bool $revoked Revoked - * - * @return static - */ - public function setRevoked(bool $revoked): static - { - $this->__set('revoked', $revoked); - return $this; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/Comments.php b/module/VuFind/src/VuFind/Db/Row/Comments.php deleted file mode 100644 index dbe7d52ad50..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/Comments.php +++ /dev/null @@ -1,161 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use DateTime; -use VuFind\Db\Entity\CommentsEntityInterface; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\UserServiceInterface; - -/** - * Row Definition for comments - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property ?int $user_id - * @property int $resource_id - * @property string $comment - * @property string $created - */ -class Comments extends RowGateway implements CommentsEntityInterface, DbServiceAwareInterface -{ - use \VuFind\Db\Service\DbServiceAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct('id', 'comments', $adapter); - } - - /** - * Id getter - * - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * Comment setter - * - * @param string $comment Comment - * - * @return static - */ - public function setComment(string $comment): static - { - $this->comment = $comment; - return $this; - } - - /** - * Comment getter - * - * @return string - */ - public function getComment(): string - { - return $this->comment; - } - - /** - * Created setter. - * - * @param DateTime $dateTime Created date - * - * @return static - */ - public function setCreated(DateTime $dateTime): static - { - $this->created = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Created getter - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); - } - - /** - * User setter. - * - * @param ?UserEntityInterface $user User that created comment - * - * @return static - */ - public function setUser(?UserEntityInterface $user): static - { - $this->user_id = $user ? $user->getId() : null; - return $this; - } - - /** - * User getter - * - * @return ?UserEntityInterface - */ - public function getUser(): ?UserEntityInterface - { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; - } - - /** - * Resource setter. - * - * @param ResourceEntityInterface $resource Resource id. - * - * @return static - */ - public function setResource(ResourceEntityInterface $resource): static - { - $this->resource_id = $resource->getId(); - return $this; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/Feedback.php b/module/VuFind/src/VuFind/Db/Row/Feedback.php deleted file mode 100644 index 960cbc621e9..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/Feedback.php +++ /dev/null @@ -1,295 +0,0 @@ - - * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -declare(strict_types=1); - -namespace VuFind\Db\Row; - -use DateTime; -use VuFind\Db\Entity\FeedbackEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\UserServiceInterface; - -/** - * Class Feedback - * - * @category VuFind - * @package Db_Row - * @author Josef Moravec - * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property string $message - * @property string $form_data - * @property string $form_name - * @property string $created - * @property string $updated - * @property int $updated_by - * @property string $status - * @property string $site_url - */ -class Feedback extends RowGateway implements FeedbackEntityInterface, DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct('id', 'feedback', $adapter); - } - - /** - * Id getter - * - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * Message setter - * - * @param string $message Message - * - * @return static - */ - public function setMessage(string $message): static - { - $this->message = $message; - return $this; - } - - /** - * Message getter - * - * @return string - */ - public function getMessage(): string - { - return $this->message; - } - - /** - * Form data setter. - * - * @param array $data Form data - * - * @return static - */ - public function setFormData(array $data): static - { - $this->form_data = json_encode($data); - return $this; - } - - /** - * Form data getter - * - * @return array - */ - public function getFormData(): array - { - return json_decode($this->form_data, true); - } - - /** - * Form name setter. - * - * @param string $name Form name - * - * @return static - */ - public function setFormName(string $name): static - { - $this->form_name = $name; - return $this; - } - - /** - * Form name getter - * - * @return string - */ - public function getFormName(): string - { - return $this->form_name; - } - - /** - * Created setter. - * - * @param DateTime $dateTime Created date - * - * @return static - */ - public function setCreated(DateTime $dateTime): static - { - $this->created = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Created getter - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); - } - - /** - * Updated setter. - * - * @param DateTime $dateTime Last update date - * - * @return static - */ - public function setUpdated(DateTime $dateTime): static - { - $this->updated = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Updated getter - * - * @return DateTime - */ - public function getUpdated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->updated); - } - - /** - * Status setter. - * - * @param string $status Status - * - * @return static - */ - public function setStatus(string $status): static - { - $this->status = $status; - return $this; - } - - /** - * Status getter - * - * @return string - */ - public function getStatus(): string - { - return $this->status; - } - - /** - * Site URL setter. - * - * @param string $url Site URL - * - * @return static - */ - public function setSiteUrl(string $url): static - { - $this->site_url = $url; - return $this; - } - - /** - * Site URL getter - * - * @return string - */ - public function getSiteUrl(): string - { - return $this->site_url; - } - - /** - * User setter. - * - * @param ?UserEntityInterface $user User that created request - * - * @return static - */ - public function setUser(?UserEntityInterface $user): static - { - $this->user_id = $user?->getId(); - return $this; - } - - /** - * User getter - * - * @return ?UserEntityInterface - */ - public function getUser(): ?UserEntityInterface - { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; - } - - /** - * Updatedby setter. - * - * @param ?UserEntityInterface $user User that updated request - * - * @return static - */ - public function setUpdatedBy(?UserEntityInterface $user): static - { - $this->updated_by = $user ? $user->getId() : null; - return $this; - } - - /** - * Updatedby getter - * - * @return ?UserEntityInterface - */ - public function getUpdatedBy(): ?UserEntityInterface - { - return $this->updated_by - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->updated_by) - : null; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/PluginManager.php b/module/VuFind/src/VuFind/Db/Row/PluginManager.php deleted file mode 100644 index 5f15b673e6f..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/PluginManager.php +++ /dev/null @@ -1,107 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ - -namespace VuFind\Db\Row; - -/** - * Database row plugin manager - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ -class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager -{ - /** - * Default plugin aliases. - * - * @var array - */ - protected $aliases = [ - 'accesstoken' => AccessToken::class, - 'changetracker' => ChangeTracker::class, - 'comments' => Comments::class, - 'externalsession' => ExternalSession::class, - 'logintoken' => LoginToken::class, - 'oairesumption' => OaiResumption::class, - 'ratings' => Ratings::class, - 'record' => Record::class, - 'resource' => Resource::class, - 'resourcetags' => ResourceTags::class, - 'search' => Search::class, - 'session' => Session::class, - 'shortlinks' => Shortlinks::class, - 'tags' => Tags::class, - 'user' => User::class, - 'usercard' => UserCard::class, - 'userlist' => UserList::class, - 'userresource' => UserResource::class, - ]; - - /** - * Default plugin factories. - * - * @var array - */ - protected $factories = [ - AccessToken::class => RowGatewayFactory::class, - AuthHash::class => RowGatewayFactory::class, - ChangeTracker::class => RowGatewayFactory::class, - Comments::class => RowGatewayFactory::class, - ExternalSession::class => RowGatewayFactory::class, - Feedback::class => RowGatewayFactory::class, - LoginToken::class => RowGatewayFactory::class, - OaiResumption::class => RowGatewayFactory::class, - Ratings::class => RowGatewayFactory::class, - Record::class => RowGatewayFactory::class, - Resource::class => RowGatewayFactory::class, - ResourceTags::class => RowGatewayFactory::class, - Search::class => RowGatewayFactory::class, - Session::class => RowGatewayFactory::class, - Shortlinks::class => RowGatewayFactory::class, - Tags::class => RowGatewayFactory::class, - User::class => UserFactory::class, - UserCard::class => RowGatewayFactory::class, - UserList::class => UserListFactory::class, - UserResource::class => RowGatewayFactory::class, - ]; - - /** - * Return the name of the base class or interface that plug-ins must conform - * to. - * - * @return string - */ - protected function getExpectedInterface() - { - return RowGateway::class; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/PrivateUser.php b/module/VuFind/src/VuFind/Db/Row/PrivateUser.php deleted file mode 100644 index 299923580fe..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/PrivateUser.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use VuFind\Auth\UserSessionPersistenceInterface; - -use function array_key_exists; - -/** - * Fake database row to represent a user in privacy mode. - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class PrivateUser extends User -{ - /** - * __get - * - * @param string $name Field to retrieve. - * - * @throws \Laminas\Db\RowGateway\Exception\InvalidArgumentException - * @return mixed - */ - public function __get($name) - { - return array_key_exists($name, $this->data) ? parent::__get($name) : null; - } - - /** - * Save - * - * @return int - */ - public function save() - { - $this->initialize(); - $this->id = -1; // fake ID - $this->getDbService(UserSessionPersistenceInterface::class)->addUserDataToSession($this); - return 1; - } - - /** - * Set session container - * - * @param \Laminas\Session\Container $session Session container - * - * @return void - * - * @deprecated No longer used or needed - */ - public function setSession(\Laminas\Session\Container $session) - { - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/Resource.php b/module/VuFind/src/VuFind/Db/Row/Resource.php deleted file mode 100644 index e369c47f80c..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/Resource.php +++ /dev/null @@ -1,409 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use VuFind\Date\DateException; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceTagsServiceInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; -use VuFind\Exception\LoginRequired as LoginRequiredException; - -use function intval; -use function strlen; - -/** - * Row Definition for resource - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $record_id - * @property string $title - * @property ?string $author - * @property ?int $year - * @property string $source - * @property ?string $extra_metadata - */ -class Resource extends RowGateway implements DbServiceAwareInterface, DbTableAwareInterface, ResourceEntityInterface -{ - use DbServiceAwareTrait; - use DbTableAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct('id', 'resource', $adapter); - } - - /** - * Remove tags from the current resource. - * - * @param \VuFind\Db\Row\User $user The user deleting the tags. - * @param string $list_id The list associated with the tags - * (optional -- omitting this will delete ALL of the user's tags). - * - * @return void - * - * @deprecated Use ResourceTagsServiceInterface::destroyResourceTagsLinksForUser() - */ - public function deleteTags($user, $list_id = null) - { - $this->getDbService(ResourceTagsServiceInterface::class) - ->destroyResourceTagsLinksForUser($this->getId(), $user, $list_id); - } - - /** - * Add a tag to the current resource. - * - * @param string $tagText The tag to save. - * @param UserEntityInterface $user The user posting the tag. - * @param string $list_id The list associated with the tag - * (optional). - * - * @return void - * - * @deprecated Use \VuFind\Tags\TagService::linkTagToResource() - */ - public function addTag($tagText, $user, $list_id = null) - { - $tagText = trim($tagText); - if (!empty($tagText)) { - $tags = $this->getDbTable('Tags'); - $tag = $tags->getByText($tagText); - - $this->getDbService(ResourceTagsServiceInterface::class)->createLink( - $this, - $tag->id, - $user, - $list_id - ); - } - } - - /** - * Remove a tag from the current resource. - * - * @param string $tagText The tag to delete. - * @param \VuFind\Db\Row\User $user The user deleting the tag. - * @param string $list_id The list associated with the tag - * (optional). - * - * @return void - * - * @deprecated Use \VuFind\Tags\TagsService::unlinkTagFromResource() - */ - public function deleteTag($tagText, $user, $list_id = null) - { - $tagText = trim($tagText); - if (!empty($tagText)) { - $tags = $this->getDbTable('Tags'); - $tagIds = []; - foreach ($tags->getByText($tagText, false, false) as $tag) { - $tagIds[] = $tag->getId(); - } - if (!empty($tagIds)) { - $this->getDbService(ResourceTagsServiceInterface::class)->destroyResourceTagsLinksForUser( - $this->getId(), - $user, - $list_id, - $tagIds - ); - } - } - } - - /** - * Add a comment to the current resource. - * - * @param string $comment The comment to save. - * @param \VuFind\Db\Row\User $user The user posting the comment. - * - * @throws LoginRequiredException - * @return int ID of newly-created comment. - */ - public function addComment($comment, $user) - { - if (!isset($user->id)) { - throw new LoginRequiredException( - "Can't add comments without logging in." - ); - } - - $table = $this->getDbTable('Comments'); - $row = $table->createRow(); - $row->setUser($user) - ->setResource($this) - ->setComment($comment) - ->setCreated(new \DateTime()); - $row->save(); - return $row->getId(); - } - - /** - * Add or update user's rating for the current resource. - * - * @param int $userId User ID - * @param ?int $rating Rating (null to delete) - * - * @throws LoginRequiredException - * @throws \Exception - * @return int ID of rating added, deleted or updated - */ - public function addOrUpdateRating(int $userId, ?int $rating): int - { - if (null !== $rating && ($rating < 0 || $rating > 100)) { - throw new \Exception('Rating value out of range'); - } - - $ratings = $this->getDbTable('Ratings'); - $callback = function ($select) use ($userId) { - $select->where->equalTo('ratings.resource_id', $this->id); - $select->where->equalTo('ratings.user_id', $userId); - }; - if ($existing = $ratings->select($callback)->current()) { - if (null === $rating) { - $existing->delete(); - } else { - $existing->rating = $rating; - $existing->save(); - } - return $existing->id; - } - - if (null === $rating) { - return 0; - } - - $row = $ratings->createRow(); - $row->user_id = $userId; - $row->resource_id = $this->id; - $row->rating = $rating; - $row->created = date('Y-m-d H:i:s'); - $row->save(); - return $row->id; - } - - /** - * Use a record driver to assign metadata to the current row. Return the - * current object to allow fluent interface. - * - * @param \VuFind\RecordDriver\AbstractBase $driver The record driver - * @param \VuFind\Date\Converter $converter Date converter - * - * @return \VuFind\Db\Row\Resource - * - * @deprecated Use \VuFind\Record\ResourcePopulator::assignMetadata() - */ - public function assignMetadata($driver, \VuFind\Date\Converter $converter) - { - // Grab title -- we have to have something in this field! - $this->title = mb_substr( - $driver->tryMethod('getSortTitle'), - 0, - 255, - 'UTF-8' - ); - if (empty($this->title)) { - $this->title = $driver->getBreadcrumb(); - } - - // Try to find an author; if not available, just leave the default null: - $author = mb_substr( - $driver->tryMethod('getPrimaryAuthor'), - 0, - 255, - 'UTF-8' - ); - if (!empty($author)) { - $this->author = $author; - } - - // Try to find a year; if not available, just leave the default null: - $dates = $driver->tryMethod('getPublicationDates'); - if (isset($dates[0]) && strlen($dates[0]) > 4) { - try { - $year = $converter->convertFromDisplayDate('Y', $dates[0]); - } catch (DateException $e) { - // If conversion fails, don't store a date: - $year = ''; - } - } else { - $year = $dates[0] ?? ''; - } - if (!empty($year)) { - $this->year = intval($year); - } - - if ($extra = $driver->tryMethod('getExtraResourceMetadata')) { - $this->extra_metadata = json_encode($extra); - } - return $this; - } - - /** - * Id getter - * - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * Record Id setter - * - * @param string $recordId recordId - * - * @return static - */ - public function setRecordId(string $recordId): static - { - $this->record_id = $recordId; - return $this; - } - - /** - * Record Id getter - * - * @return string - */ - public function getRecordId(): string - { - return $this->record_id; - } - - /** - * Title setter - * - * @param string $title Title of the record. - * - * @return static - */ - public function setTitle(string $title): static - { - $this->title = $title; - return $this; - } - - /** - * Title getter - * - * @return string - */ - public function getTitle(): string - { - return $this->title; - } - - /** - * Author setter - * - * @param ?string $author Author of the title. - * - * @return static - */ - public function setAuthor(?string $author): static - { - $this->author = $author; - return $this; - } - - /** - * Year setter - * - * @param ?int $year Year title is published. - * - * @return static - */ - public function setYear(?int $year): static - { - $this->year = $year; - return $this; - } - - /** - * Source setter - * - * @param string $source Source (a search backend ID). - * - * @return static - */ - public function setSource(string $source): static - { - $this->source = $source; - return $this; - } - - /** - * Source getter - * - * @return string - */ - public function getSource(): string - { - return $this->source; - } - - /** - * Extra Metadata setter - * - * @param ?string $extraMetadata ExtraMetadata. - * - * @return static - */ - public function setExtraMetadata(?string $extraMetadata): static - { - $this->extra_metadata = $extraMetadata; - return $this; - } - - /** - * Extra Metadata getter - * - * @return ?string - */ - public function getExtraMetadata(): ?string - { - return $this->extra_metadata; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/RowGatewayFactory.php b/module/VuFind/src/VuFind/Db/Row/RowGatewayFactory.php deleted file mode 100644 index ea294c38235..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/RowGatewayFactory.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Row; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * Generic row gateway factory. - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class RowGatewayFactory implements \Laminas\ServiceManager\Factory\FactoryInterface -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - $adapter = $container->get(\Laminas\Db\Adapter\Adapter::class); - return new $requestedName($adapter, ...($options ?? [])); - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/Search.php b/module/VuFind/src/VuFind/Db/Row/Search.php deleted file mode 100644 index 0f5bac540d1..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/Search.php +++ /dev/null @@ -1,424 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use DateTime; -use VuFind\Crypt\HMAC; -use VuFind\Db\Entity\SearchEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\UserServiceInterface; - -use function is_resource; - -/** - * Row Definition for search - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property ?string $session_id - * @property string $created - * @property ?string $title - * @property int $saved - * @property string $search_object - * @property ?int $checksum - * @property int $notification_frequency - * @property string $last_notification_sent - * @property string $notification_base_url - */ -class Search extends RowGateway implements - SearchEntityInterface, - \VuFind\Db\Table\DbTableAwareInterface, - DbServiceAwareInterface -{ - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct('id', 'search', $adapter); - } - - /** - * Support method to make sure that the search_object field is formatted as a - * string, since PostgreSQL sometimes represents it as a resource. - * - * @return void - */ - protected function normalizeSearchObject() - { - // Note that if we have a resource, we need to grab the contents before - // saving -- this is necessary for PostgreSQL compatibility although MySQL - // returns a plain string - if (is_resource($this->search_object)) { - $this->search_object = stream_get_contents($this->search_object); - } - } - - /** - * Get the search object from the row. - * - * @return ?\VuFind\Search\Minified - */ - public function getSearchObject(): ?\VuFind\Search\Minified - { - // We need to make sure the search object is a string before unserializing: - $this->normalizeSearchObject(); - return $this->search_object ? unserialize($this->search_object) : null; - } - - /** - * Get the search object from the row, and throw an exception if it is missing. - * - * @return \VuFind\Search\Minified - * @throws \Exception - * - * @deprecated - */ - public function getSearchObjectOrThrowException(): \VuFind\Search\Minified - { - if (!($result = $this->getSearchObject())) { - throw new \Exception('Problem decoding saved search'); - } - return $result; - } - - /** - * Save - * - * @return int - */ - public function save() - { - // We can't save if the search object is a resource; make sure it's a - // string first: - $this->normalizeSearchObject(); - return parent::save(); - } - - /** - * Set last executed time for scheduled alert. - * - * @param string $time Time. - * - * @return mixed - * - * @deprecated - */ - public function setLastExecuted($time) - { - $this->last_notification_sent = $time; - return $this->save(); - } - - /** - * Set schedule for scheduled alert. - * - * @param int $schedule Schedule. - * @param string $url Site base URL - * - * @return mixed - * - * @deprecated - */ - public function setSchedule($schedule, $url = null) - { - $this->notification_frequency = $schedule; - if ($url) { - $this->notification_base_url = $url; - } - return $this->save(); - } - - /** - * Utility function for generating a token for unsubscribing a - * saved search. - * - * @param HMAC $hmac HMAC hash generator - * @param UserEntityInterface $user User object - * - * @return string token - * - * @deprecated Use \VuFind\Crypt\SecretCalculator::getSearchUnsubscribeSecret() - */ - public function getUnsubscribeSecret(HMAC $hmac, $user) - { - $data = [ - 'id' => $this->id, - 'user_id' => $user->getId(), - 'created' => $user->getCreated()->format('Y-m-d H:i:s'), - ]; - return $hmac->generate(array_keys($data), $data); - } - - /** - * Get identifier (returns null for an uninitialized or non-persisted object). - * - * @return ?int - */ - public function getId(): ?int - { - return $this->id ?? null; - } - - /** - * Get user. - * - * @return ?UserEntityInterface - */ - public function getUser(): ?UserEntityInterface - { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; - } - - /** - * Set user. - * - * @param ?UserEntityInterface $user User - * - * @return static - */ - public function setUser(?UserEntityInterface $user): static - { - $this->user_id = $user?->getId(); - return $this; - } - - /** - * Get session identifier. - * - * @return ?string - */ - public function getSessionId(): ?string - { - return $this->session_id ?? null; - } - - /** - * Set session identifier. - * - * @param ?string $sessionId Session id - * - * @return static - */ - public function setSessionId(?string $sessionId): static - { - $this->session_id = $sessionId; - return $this; - } - - /** - * Get created date. - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); - } - - /** - * Set created date. - * - * @param DateTime $dateTime Created date - * - * @return static - */ - public function setCreated(DateTime $dateTime): static - { - $this->created = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Get title. - * - * @return ?string - */ - public function getTitle(): ?string - { - return $this->title ?? null; - } - - /** - * Set title. - * - * @param ?string $title Title - * - * @return static - */ - public function setTitle(?string $title): static - { - $this->title = $title; - return $this; - } - - /** - * Get saved. - * - * @return bool - */ - public function getSaved(): bool - { - return (bool)($this->saved ?? 0); - } - - /** - * Set saved. - * - * @param bool $saved Saved - * - * @return static - */ - public function setSaved(bool $saved): static - { - $this->saved = $saved ? 1 : 0; - return $this; - } - - /** - * Set search object. - * - * @param ?\VuFind\Search\Minified $searchObject Search object - * - * @return static - */ - public function setSearchObject(?\VuFind\Search\Minified $searchObject): static - { - $this->search_object = $searchObject ? serialize($searchObject) : null; - return $this; - } - - /** - * Get checksum. - * - * @return ?int - */ - public function getChecksum(): ?int - { - return $this->checksum ?? null; - } - - /** - * Set checksum. - * - * @param ?int $checksum Checksum - * - * @return static - */ - public function setChecksum(?int $checksum): static - { - $this->checksum = $checksum; - return $this; - } - - /** - * Get notification frequency. - * - * @return int - */ - public function getNotificationFrequency(): int - { - return $this->notification_frequency ?? 0; - } - - /** - * Set notification frequency. - * - * @param int $notificationFrequency Notification frequency - * - * @return static - */ - public function setNotificationFrequency(int $notificationFrequency): static - { - $this->notification_frequency = $notificationFrequency; - return $this; - } - - /** - * When was the last notification sent? - * - * @return DateTime - */ - public function getLastNotificationSent(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->last_notification_sent); - } - - /** - * Set when last notification was sent. - * - * @param DateTime $lastNotificationSent Time when last notification was sent - * - * @return static - */ - public function setLastNotificationSent(Datetime $lastNotificationSent): static - { - $this->last_notification_sent = $lastNotificationSent->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Get notification base URL. - * - * @return string - */ - public function getNotificationBaseUrl(): string - { - return $this->notification_base_url ?? ''; - } - - /** - * Set notification base URL. - * - * @param string $notificationBaseUrl Notification base URL - * - * @return static - */ - public function setNotificationBaseUrl(string $notificationBaseUrl): static - { - $this->notification_base_url = $notificationBaseUrl; - return $this; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/Tags.php b/module/VuFind/src/VuFind/Db/Row/Tags.php deleted file mode 100644 index b36457a3c06..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/Tags.php +++ /dev/null @@ -1,149 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Db\Entity\TagsEntityInterface; -use VuFind\Db\Table\Resource as ResourceTable; - -/** - * Row Definition for tags - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property string $tag - */ -class Tags extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface, TagsEntityInterface -{ - use \VuFind\Db\Table\DbTableAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - */ - public function __construct($adapter) - { - parent::__construct('id', 'tags', $adapter); - } - - /** - * Get all resources associated with the current tag. - * - * @param string $source Record source (optional limiter) - * @param string $sort Resource field to sort on (optional) - * @param int $offset Offset for results - * @param int $limit Limit for results (null for none) - * - * @return array - */ - public function getResources( - $source = null, - $sort = null, - $offset = 0, - $limit = null - ) { - // Set up base query: - $tag = $this; - $callback = function ($select) use ($tag, $source, $sort, $offset, $limit) { - $columns = [ - new Expression( - 'DISTINCT(?)', - ['resource.id'], - [Expression::TYPE_IDENTIFIER] - ), Select::SQL_STAR, - ]; - $select->columns($columns); - $select->join( - ['rt' => 'resource_tags'], - 'resource.id = rt.resource_id', - [] - ); - $select->where->equalTo('rt.tag_id', $tag->id); - - if (!empty($source)) { - $select->where->equalTo('source', $source); - } - - if (!empty($sort)) { - ResourceTable::applySort($select, $sort, 'resource', $columns); - } - - if ($offset > 0) { - $select->offset($offset); - } - if (null !== $limit) { - $select->limit($limit); - } - }; - - $table = $this->getDbTable('Resource'); - return $table->select($callback); - } - - /** - * Id getter - * - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * Tag setter - * - * @param string $tag Tag - * - * @return static - */ - public function setTag(string $tag): static - { - $this->tag = $tag; - return $this; - } - - /** - * Tag getter - * - * @return string - */ - public function getTag(): string - { - return $this->tag; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/User.php b/module/VuFind/src/VuFind/Db/Row/User.php deleted file mode 100644 index 4551893347e..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/User.php +++ /dev/null @@ -1,1195 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use DateTime; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Auth\ILSAuthenticator; -use VuFind\Config\AccountCapabilities; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\ResourceServiceInterface; -use VuFind\Db\Service\ResourceTagsService; -use VuFind\Db\Service\ResourceTagsServiceInterface; -use VuFind\Db\Service\TagServiceInterface; -use VuFind\Db\Service\UserCardServiceInterface; -use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Db\Service\UserResourceServiceInterface; -use VuFind\Db\Service\UserServiceInterface; -use VuFind\Favorites\FavoritesService; - -use function count; - -/** - * Row Definition for user - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property ?string $username - * @property string $password - * @property ?string $pass_hash - * @property string $firstname - * @property string $lastname - * @property string $email - * @property ?string $email_verified - * @property string $pending_email - * @property int $user_provided_email - * @property ?string $cat_id - * @property ?string $cat_username - * @property ?string $cat_password - * @property ?string $cat_pass_enc - * @property string $college - * @property string $major - * @property ?string $home_library - * @property string $created - * @property string $verify_hash - * @property string $last_login - * @property ?string $auth_method - * @property string $last_language - */ -class User extends RowGateway implements - UserEntityInterface, - \VuFind\Db\Service\DbServiceAwareInterface, - \VuFind\Db\Table\DbTableAwareInterface -{ - use \VuFind\Db\Service\DbServiceAwareTrait; - use \VuFind\Db\Table\DbTableAwareTrait; - - /** - * VuFind configuration - * - * @var \VuFind\Config\Config - */ - protected $config = null; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - * @param ILSAuthenticator $ilsAuthenticator ILS authenticator - * @param AccountCapabilities $capabilities Account capabilities configuration (null for defaults) - * @param FavoritesService $favoritesService Favorites service - */ - public function __construct( - $adapter, - protected ILSAuthenticator $ilsAuthenticator, - protected AccountCapabilities $capabilities, - protected FavoritesService $favoritesService, - ) { - parent::__construct('id', 'user', $adapter); - } - - /** - * Configuration setter - * - * @param \VuFind\Config\Config $config VuFind configuration - * - * @return void - * - * @deprecated - */ - public function setConfig(\VuFind\Config\Config $config) - { - $this->config = $config; - } - - /** - * Reset ILS login credentials. - * - * @return void - * - * @deprecated Use setCatUsername(null)->setRawCatPassword(null)->setCatPassEnc(null) - */ - public function clearCredentials() - { - $this->cat_username = null; - $this->cat_password = null; - $this->cat_pass_enc = null; - } - - /** - * Save ILS ID. - * - * @param string $catId Catalog ID to save. - * - * @return mixed The output of the save method. - * @throws \VuFind\Exception\PasswordSecurity - * - * @deprecated Use UserEntityInterface::setCatId() and \VuFind\Db\Service\DbServiceInterface::persistEntity() - */ - public function saveCatalogId($catId) - { - $this->cat_id = $catId; - return $this->save(); - } - - /** - * Set ILS login credentials without saving them. - * - * @param string $username Username to save - * @param ?string $password Password to save (null for none) - * - * @return void - * - * @deprecated Use ILSAuthenticator::setUserCatalogCredentials() - */ - public function setCredentials($username, $password) - { - $this->ilsAuthenticator->setUserCatalogCredentials($this, $username, $password); - } - - /** - * Save ILS login credentials. - * - * @param string $username Username to save - * @param string $password Password to save - * - * @return void - * @throws \VuFind\Exception\PasswordSecurity - * - * @deprecated Use ILSAuthenticator::saveUserCatalogCredentials() - */ - public function saveCredentials($username, $password) - { - $this->ilsAuthenticator->saveUserCatalogCredentials($this, $username, $password); - } - - /** - * Save date/time when email address has been verified. - * - * @param string $datetime optional date/time to save. - * - * @return mixed The output of the save method. - * - * @deprecated Use UserEntityInterface::setEmailVerified() and - * \VuFind\Db\Service\DbServiceInterface::persistEntity() - */ - public function saveEmailVerified($datetime = null) - { - if ($datetime === null) { - $datetime = date('Y-m-d H:i:s'); - } - - $this->email_verified = $datetime; - return $this->save(); - } - - /** - * This is a getter for the Catalog Password. It will return a plaintext version - * of the password. - * - * @return string The Catalog password in plain text - * @throws \VuFind\Exception\PasswordSecurity - * - * @deprecated Use ILSAuthenticator::getCatPasswordForUser() - */ - public function getCatPassword() - { - return $this->ilsAuthenticator->getCatPasswordForUser($this); - } - - /** - * Is ILS password encryption enabled? - * - * @return bool - * - * @deprecated - */ - protected function passwordEncryptionEnabled() - { - return $this->ilsAuthenticator->passwordEncryptionEnabled(); - } - - /** - * This is a central function for encrypting and decrypting so that - * logic is all in one location - * - * @param string $text The text to be encrypted or decrypted - * @param bool $encrypt True if we wish to encrypt text, False if we wish to - * decrypt text. - * - * @return string|bool The encrypted/decrypted string - * @throws \VuFind\Exception\PasswordSecurity - * - * @deprecated Use ILSAuthenticator::encrypt() or ILSAuthenticator::decrypt() - */ - protected function encryptOrDecrypt($text, $encrypt = true) - { - $method = $encrypt ? 'encrypt' : 'decrypt'; - return $this->ilsAuthenticator->$method($text); - } - - /** - * Change home library. - * - * @param ?string $homeLibrary New home library to store, or null to indicate - * that the user does not want a default. An empty string is the default for - * backward compatibility and indicates that system's default pick up location is - * to be used - * - * @return mixed The output of the save method. - * - * @deprecated Use ILSAuthenticator::updateUserHomeLibrary() - */ - public function changeHomeLibrary($homeLibrary) - { - return $this->ilsAuthenticator->updateUserHomeLibrary($this, $homeLibrary); - } - - /** - * Check whether the email address has been verified yet. - * - * @return bool - * - * @deprecated Use getEmailVerified() - */ - public function checkEmailVerified() - { - return !empty($this->email_verified); - } - - /** - * Get a list of all tags generated by the user in favorites lists. Note that - * the returned list WILL NOT include tags attached to records that are not - * saved in favorites lists. - * - * @param string $resourceId Filter for tags tied to a specific resource (null for no filter). - * @param int $listId Filter for tags tied to a specific list (null for no filter). - * @param string $source Filter for tags tied to a specific record source. (null for no filter). - * - * @return array - * - * @deprecated Use TagServiceInterface::getUserTagsFromFavorites() - */ - public function getTags($resourceId = null, $listId = null, $source = null) - { - return $this->getDbTable('Tags')->getListTagsForUser($this->getId(), $resourceId, $listId, $source); - } - - /** - * Get tags assigned by the user to a favorite list. - * - * @param int $listId List id - * - * @return array - * - * @deprecated Use TagServiceInterface::getListTags() - */ - public function getListTags($listId) - { - return $this->getDbTable('Tags')->getForList($listId, $this->getId()); - } - - /** - * Same as getTags(), but returns a string for use in edit mode rather than an - * array of tag objects. - * - * @param string $resourceId Filter for tags tied to a specific resource (null - * for no filter). - * @param int $listId Filter for tags tied to a specific list (null for no - * filter). - * @param string $source Filter for tags tied to a specific record source - * (null for no filter). - * - * @return string - * - * @deprecated Use \VuFind\Favorites\FavoritesService::getTagStringForEditing() - */ - public function getTagString($resourceId = null, $listId = null, $source = null) - { - return $this->formatTagString($this->getTags($resourceId, $listId, $source)); - } - - /** - * Same as getTagString(), but operates on a list of tags. - * - * @param array $tags Tags - * - * @return string - * - * @deprecated Use \VuFind\Favorites\FavoritesService::formatTagStringForEditing() - */ - public function formatTagString($tags) - { - $tagStr = ''; - if (count($tags) > 0) { - foreach ($tags as $tag) { - if (strstr($tag['tag'], ' ')) { - $tagStr .= "\"{$tag['tag']}\" "; - } else { - $tagStr .= "{$tag['tag']} "; - } - } - } - return trim($tagStr); - } - - /** - * Get all of the lists associated with this user. - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - * - * @deprecated Use UserListServiceInterface::getUserListsAndCountsByUser() - */ - public function getLists() - { - $userId = $this->id; - $callback = function ($select) use ($userId) { - $select->columns( - [ - Select::SQL_STAR, - 'cnt' => new Expression( - 'COUNT(DISTINCT(?))', - ['ur.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['ur' => 'user_resource'], - 'user_list.id = ur.list_id', - [], - $select::JOIN_LEFT - ); - $select->where->equalTo('user_list.user_id', $userId); - $select->group( - [ - 'user_list.id', 'user_list.user_id', 'title', 'description', - 'created', 'public', - ] - ); - $select->order(['title']); - }; - - $table = $this->getDbTable('UserList'); - return $table->select($callback); - } - - /** - * Get information saved in a user's favorites for a particular record. - * - * @param string $resourceId ID of record being checked. - * @param int $listId Optional list ID (to limit results to a particular - * list). - * @param string $source Source of record to look up - * - * @return array - * - * @deprecated Use UserResourceServiceInterface::getFavoritesForRecord() - */ - public function getSavedData( - $resourceId, - $listId = null, - $source = DEFAULT_SEARCH_BACKEND - ) { - $table = $this->getDbTable('UserResource'); - return $table->getSavedData($resourceId, $source, $listId, $this->id); - } - - /** - * Add/update a resource in the user's account. - * - * @param \VuFind\Db\Row\Resource $resource The resource to add/update - * @param \VuFind\Db\Row\UserList $list The list to store the resource - * in. - * @param array $tagArray An array of tags to associate - * with the resource. - * @param string $notes User notes about the resource. - * @param bool $replaceExisting Whether to replace all - * existing tags (true) or append to the existing list (false). - * - * @return void - * - * @deprecated Use \VuFind\Favorites\FavoritesService::saveResourceToFavorites() - */ - public function saveResource( - $resource, - $list, - $tagArray, - $notes, - $replaceExisting = true - ) { - // Create the resource link if it doesn't exist and update the notes in any case: - $this->getDbService(UserResourceServiceInterface::class)->createOrUpdateLink($resource, $this, $list, $notes); - - // If we're replacing existing tags, delete the old ones before adding the - // new ones: - if ($replaceExisting) { - $this->getDbService(ResourceTagsService::class) - ->destroyResourceTagsLinksForUser($resource->getId(), $this, $list); - } - - // Add the new tags: - foreach ($tagArray as $tag) { - $resource->addTag($tag, $this, $list->id); - } - } - - /** - * Given an array of item ids, remove them from all lists - * - * @param array $ids IDs to remove from the list - * @param string $source Type of resource identified by IDs - * - * @return void - * - * @deprecated Use \VuFind\Favorites\FavoritesService::removeUserResourcesById() - */ - public function removeResourcesById($ids, $source = DEFAULT_SEARCH_BACKEND) - { - // Retrieve a list of resource IDs: - $resources = $this->getDbService(ResourceServiceInterface::class)->getResourcesByRecordIds($ids, $source); - - $resourceIDs = []; - foreach ($resources as $current) { - $resourceIDs[] = $current->getId(); - } - - // Remove Resource (related tags are also removed implicitly) - $userResourceTable = $this->getDbTable('UserResource'); - // true here makes sure that only tags in lists are deleted - $userResourceTable->destroyLinks($resourceIDs, $this->id, true); - } - - /** - * Whether library cards are enabled - * - * @return bool - * - * @deprecated use \VuFind\Config\AccountCapabilities::libraryCardsEnabled() - */ - public function libraryCardsEnabled() - { - return $this->capabilities->libraryCardsEnabled(); - } - - /** - * Get all library cards associated with the user. - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - * @throws \VuFind\Exception\LibraryCard - * - * @deprecated Use UserCardServiceInterface::getLibraryCards() - */ - public function getLibraryCards() - { - if (!$this->capabilities->libraryCardsEnabled()) { - return new \Laminas\Db\ResultSet\ResultSet(); - } - $userCard = $this->getDbTable('UserCard'); - return $userCard->select(['user_id' => $this->id]); - } - - /** - * Get library card data - * - * @param int $id Library card ID - * - * @return UserCard|false Card data if found, false otherwise - * @throws \VuFind\Exception\LibraryCard - * - * @deprecated Use LibraryCardServiceInterface::getOrCreateLibraryCard() - */ - public function getLibraryCard($id = null) - { - return $this->getUserCardService()->getOrCreateLibraryCard($this, $id); - } - - /** - * Delete library card - * - * @param int $id Library card ID - * - * @return void - * @throws \VuFind\Exception\LibraryCard - * - * @deprecated Use UserCardServiceInterface::deleteLibraryCard() - */ - public function deleteLibraryCard($id) - { - return $this->getUserCardService()->deleteLibraryCard($this, $id); - } - - /** - * Activate a library card for the given username - * - * @param int $id Library card ID - * - * @return void - * @throws \VuFind\Exception\LibraryCard - * - * @deprecated Use UserCardServiceInterface::activateLibraryCard() - */ - public function activateLibraryCard($id) - { - return $this->getUserCardService()->activateLibraryCard($this, $id); - } - - /** - * Save library card with the given information - * - * @param int $id Card ID - * @param string $cardName Card name - * @param string $username Username - * @param string $password Password - * @param string $homeLib Home Library - * - * @return int Card ID - * @throws \VuFind\Exception\LibraryCard - * - * @deprecated Use UserCardServiceInterface::persistLibraryCardData() - */ - public function saveLibraryCard( - $id, - $cardName, - $username, - $password, - $homeLib = '' - ) { - return $this->getUserCardService() - ->persistLibraryCardData($this, $id, $cardName, $username, $password, $homeLib) - ->getId(); - } - - /** - * Verify that the current card information exists in user's library cards - * (if enabled) and is up to date. - * - * @return void - * @throws \VuFind\Exception\PasswordSecurity - * - * @deprecated Use UserCardServiceInterface::synchronizeUserLibraryCardData() - */ - protected function updateLibraryCardEntry() - { - $this->getUserCardService()->synchronizeUserLibraryCardData($this); - } - - /** - * Get a UserCard service object. - * - * @return UserCardServiceInterface - */ - protected function getUserCardService() - { - return $this->getDbService(UserCardServiceInterface::class); - } - - /** - * Destroy the user. - * - * @param bool $removeComments Whether to remove user's comments - * @param bool $removeRatings Whether to remove user's ratings - * - * @return int The number of rows deleted. - * - * @deprecated Use \VuFind\Account\UserAccountService::purgeUserData() - */ - public function delete($removeComments = true, $removeRatings = true) - { - // Remove all lists owned by the user: - $listService = $this->getDbService(UserListServiceInterface::class); - foreach ($listService->getUserListsByUser($this) as $current) { - $this->favoritesService->destroyList($current, $this, true); - } - $this->getDbService(ResourceTagsServiceInterface::class)->destroyResourceTagsLinksForUser(null, $this); - if ($removeComments) { - $comments = $this->getDbService( - \VuFind\Db\Service\CommentsServiceInterface::class - ); - $comments->deleteByUser($this->getId()); - } - if ($removeRatings) { - $ratings = $this->getDbService(\VuFind\Db\Service\RatingsServiceInterface::class); - $ratings->deleteByUser($this); - } - - // Remove the user itself: - return parent::delete(); - } - - /** - * Update the verification hash for this user - * - * @return bool save success - * - * @deprecated Use \VuFind\Auth\Manager::updateUserVerifyHash() - */ - public function updateHash() - { - $hash = md5($this->username . $this->password . $this->pass_hash . rand()); - // Make totally sure the timestamp is exactly 10 characters: - $time = str_pad(substr((string)time(), 0, 10), 10, '0', STR_PAD_LEFT); - $this->verify_hash = $hash . $time; - return $this->save(); - } - - /** - * Updated saved language - * - * @param string $language New language - * - * @return void - * - * @deprecated Use \VuFind\Db\Entity\UserEntityInterface::setLastLanguage() - * and \VuFind\Db\Service\UserService::persistEntity() instead. - */ - public function updateLastLanguage($language) - { - $this->last_language = $language; - $this->save(); - } - - /** - * Update the user's email address, if appropriate. Note that this does NOT - * automatically save the row; it assumes a subsequent call will be made to - * persist the data. - * - * @param string $email New email address - * @param bool $userProvided Was this email provided by the user (true) or - * an automated lookup (false)? - * - * @return void - * - * @deprecated Use \VuFind\Db\Service\UserServiceInterface::updateUserEmail() - */ - public function updateEmail($email, $userProvided = false) - { - $this->getDbService(UserServiceInterface::class)->updateUserEmail($this, $email, $userProvided); - } - - /** - * Get the list of roles of this identity - * - * @return string[]|\Rbac\Role\RoleInterface[] - */ - public function getRoles() - { - return ['loggedin']; - } - - /** - * Get identifier (returns null for an uninitialized or non-persisted object). - * - * @return ?int - */ - public function getId(): ?int - { - return $this->id; - } - - /** - * Username setter - * - * @param string $username Username - * - * @return static - */ - public function setUsername(string $username): static - { - $this->username = $username; - return $this; - } - - /** - * Get username. - * - * @return string - */ - public function getUsername(): string - { - return $this->username; - } - - /** - * Set raw (unhashed) password (if available). This should only be used when hashing is disabled. - * - * @param string $password Password - * - * @return static - */ - public function setRawPassword(string $password): static - { - $this->password = $password; - return $this; - } - - /** - * Get raw (unhashed) password (if available). This should only be used when hashing is disabled. - * - * @return string - */ - public function getRawPassword(): string - { - return $this->password ?? ''; - } - - /** - * Set hashed password. This should only be used when hashing is enabled. - * - * @param ?string $hash Password hash - * - * @return static - */ - public function setPasswordHash(?string $hash): static - { - $this->pass_hash = $hash; - return $this; - } - - /** - * Get hashed password. This should only be used when hashing is enabled. - * - * @return ?string - */ - public function getPasswordHash(): ?string - { - return $this->pass_hash ?? null; - } - - /** - * Set firstname. - * - * @param string $firstName New first name - * - * @return static - */ - public function setFirstname(string $firstName): static - { - $this->firstname = $firstName; - return $this; - } - - /** - * Get firstname. - * - * @return string - */ - public function getFirstname(): string - { - return $this->firstname; - } - - /** - * Set lastname. - * - * @param string $lastName New last name - * - * @return static - */ - public function setLastname(string $lastName): static - { - $this->lastname = $lastName; - return $this; - } - - /** - * Get lastname. - * - * @return string - */ - public function getLastname(): string - { - return $this->lastname; - } - - /** - * Set email. - * - * @param string $email Email address - * - * @return static - */ - public function setEmail(string $email): static - { - $this->email = $email; - return $this; - } - - /** - * Get email. - * - * @return string - */ - public function getEmail(): string - { - return $this->email; - } - - /** - * Set pending email. - * - * @param string $email New pending email - * - * @return static - */ - public function setPendingEmail(string $email): static - { - $this->pending_email = $email; - return $this; - } - - /** - * Get pending email. - * - * @return string - */ - public function getPendingEmail(): string - { - return $this->pending_email ?? ''; - } - - /** - * Catalog id setter - * - * @param ?string $catId Catalog id - * - * @return static - */ - public function setCatId(?string $catId): static - { - $this->cat_id = $catId; - return $this; - } - - /** - * Get catalog id. - * - * @return ?string - */ - public function getCatId(): ?string - { - return $this->cat_id; - } - - /** - * Catalog username setter - * - * @param ?string $catUsername Catalog username - * - * @return static - */ - public function setCatUsername(?string $catUsername): static - { - $this->cat_username = $catUsername; - return $this; - } - - /** - * Get catalog username. - * - * @return ?string - */ - public function getCatUsername(): ?string - { - return $this->cat_username ?? ''; - } - - /** - * Home library setter - * - * @param ?string $homeLibrary Home library - * - * @return static - */ - public function setHomeLibrary(?string $homeLibrary): static - { - $this->home_library = $homeLibrary; - return $this; - } - - /** - * Get home library. - * - * @return ?string - */ - public function getHomeLibrary(): ?string - { - return $this->home_library; - } - - /** - * Raw catalog password setter - * - * @param ?string $catPassword Cat password - * - * @return static - */ - public function setRawCatPassword(?string $catPassword): static - { - $this->cat_password = $catPassword; - return $this; - } - - /** - * Get raw catalog password. - * - * @return ?string - */ - public function getRawCatPassword(): ?string - { - return $this->cat_password ?? null; - } - - /** - * Encrypted catalog password setter - * - * @param ?string $passEnc Encrypted password - * - * @return static - */ - public function setCatPassEnc(?string $passEnc): static - { - $this->cat_pass_enc = $passEnc; - return $this; - } - - /** - * Get encrypted catalog password. - * - * @return ?string - */ - public function getCatPassEnc(): ?string - { - return $this->cat_pass_enc ?? null; - } - - /** - * Set college. - * - * @param string $college College - * - * @return static - */ - public function setCollege(string $college): static - { - $this->college = $college; - return $this; - } - - /** - * Get college. - * - * @return string - */ - public function getCollege(): string - { - return $this->college ?? ''; - } - - /** - * Set major. - * - * @param string $major Major - * - * @return static - */ - public function setMajor(string $major): static - { - $this->major = $major; - return $this; - } - - /** - * Get major. - * - * @return string - */ - public function getMajor(): string - { - return $this->major ?? ''; - } - - /** - * Set verification hash for recovery. - * - * @param string $hash Hash value to save - * - * @return static - */ - public function setVerifyHash(string $hash): static - { - $this->verify_hash = $hash; - return $this; - } - - /** - * Get verification hash for recovery. - * - * @return string - */ - public function getVerifyHash(): string - { - return $this->verify_hash ?? ''; - } - - /** - * Set active authentication method (if any). - * - * @param ?string $authMethod New value (null for none) - * - * @return static - */ - public function setAuthMethod(?string $authMethod): static - { - $this->auth_method = $authMethod; - return $this; - } - - /** - * Get active authentication method (if any). - * - * @return ?string - */ - public function getAuthMethod(): ?string - { - return $this->auth_method; - } - - /** - * Set last language. - * - * @param string $lang Last language - * - * @return static - */ - public function setLastLanguage(string $lang): static - { - $this->last_language = $lang; - return $this; - } - - /** - * Get last language. - * - * @return string - */ - public function getLastLanguage(): string - { - return $this->last_language ?? ''; - } - - /** - * Does the user have a user-provided (true) vs. automatically looked up (false) email address? - * - * @return bool - */ - public function hasUserProvidedEmail(): bool - { - return (bool)($this->user_provided_email ?? false); - } - - /** - * Set the flag indicating whether the email address is user-provided. - * - * @param bool $userProvided New value - * - * @return static - */ - public function setHasUserProvidedEmail(bool $userProvided): static - { - $this->user_provided_email = $userProvided ? 1 : 0; - return $this; - } - - /** - * Last login setter. - * - * @param DateTime $dateTime Last login date - * - * @return static - */ - public function setLastLogin(DateTime $dateTime): static - { - $this->last_login = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Last login getter - * - * @return DateTime - */ - public function getLastLogin(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->last_login); - } - - /** - * Created setter - * - * @param DateTime $dateTime Creation date - * - * @return static - */ - public function setCreated(DateTime $dateTime): static - { - $this->created = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Created getter - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); - } - - /** - * Set email verification date (or null for unverified). - * - * @param ?DateTime $dateTime Verification date (or null) - * - * @return static - */ - public function setEmailVerified(?DateTime $dateTime): static - { - $this->email_verified = $dateTime?->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Get email verification date (or null for unverified). - * - * @return ?DateTime - */ - public function getEmailVerified(): ?DateTime - { - return $this->email_verified ? DateTime::createFromFormat('Y-m-d H:i:s', $this->email_verified) : null; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/UserFactory.php b/module/VuFind/src/VuFind/Db/Row/UserFactory.php deleted file mode 100644 index e1299e16555..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/UserFactory.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Row; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; -use VuFind\Favorites\FavoritesService; - -/** - * User row gateway factory. - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class UserFactory extends RowGatewayFactory -{ - /** - * Class name for private user class. - * - * @var string - */ - protected $privateUserClass = __NAMESPACE__ . '\PrivateUser'; - - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - $config = $container->get(\VuFind\Config\PluginManager::class)->get('config'); - $privacy = $config->Authentication->privacy ?? false; - $capabilities = $container->get(\VuFind\Config\AccountCapabilities::class); - $rowClass = $privacy ? $this->privateUserClass : $requestedName; - $ilsAuthenticator = $container->get(\VuFind\Auth\ILSAuthenticator::class); - $favoritesService = $container->get(FavoritesService::class); - return parent::__invoke($container, $rowClass, [$ilsAuthenticator, $capabilities, $favoritesService]); - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/UserList.php b/module/VuFind/src/VuFind/Db/Row/UserList.php deleted file mode 100644 index a6c9471ef30..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/UserList.php +++ /dev/null @@ -1,367 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Row; - -use DateTime; -use Laminas\Session\Container; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; -use VuFind\Db\Service\ResourceTagsServiceInterface; -use VuFind\Db\Service\UserServiceInterface; -use VuFind\Exception\ListPermission as ListPermissionException; -use VuFind\Tags\TagsService; - -/** - * Row Definition for user_list - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - * - * @property int $id - * @property int $user_id - * @property string $title - * @property string $description - * @property string $created - * @property bool $public - */ -class UserList extends RowGateway implements - \VuFind\Db\Table\DbTableAwareInterface, - UserListEntityInterface, - DbServiceAwareInterface -{ - use \VuFind\Db\Table\DbTableAwareTrait; - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter - * @param TagsService $tagsService Tags service - * @param ?Container $session Session container for last list information - */ - public function __construct($adapter, protected TagsService $tagsService, protected ?Container $session = null) - { - parent::__construct('id', 'user_list', $adapter); - } - - /** - * Is the current user allowed to edit this list? - * - * @param ?UserEntityInterface $user Logged-in user (null if none) - * - * @return bool - * - * @deprecated Use \VuFind\Favorites\FavoritesService::userCanEditList() - */ - public function editAllowed($user) - { - if ($user && $user->id == $this->user_id) { - return true; - } - return false; - } - - /** - * Get an array of resource tags associated with this list. - * - * @return array - */ - public function getResourceTags() - { - $table = $this->getDbTable('User'); - $user = $table->select(['id' => $this->user_id])->current(); - if (empty($user)) { - return []; - } - return $user->getTags(null, $this->id); - } - - /** - * Get an array of tags assigned to this list. - * - * @return array - * - * @deprecated Use \VuFind\Db\Service\TagServiceInterface::getListTags() - */ - public function getListTags() - { - return $this->getDbTable('Tags')->getForList($this->getId(), $this->getUser()->getId()); - } - - /** - * Add a tag to the list. - * - * @param string $tagText The tag to save. - * @param UserEntityInterface $user The user posting the tag. - * - * @return void - * - * @deprecated Use \VuFind\Favorites\FavoritesService::addListTag() - */ - public function addListTag($tagText, $user) - { - $tagText = trim($tagText); - if (!empty($tagText)) { - $tags = $this->getDbTable('tags'); - $tag = $tags->getByText($tagText); - $this->getDbService(ResourceTagsServiceInterface::class)->createLink( - null, - $tag->id, - $user, - $this - ); - } - } - - /** - * Set session container. - * - * @param Container $session Session container - * - * @return void - */ - public function setSession(Container $session) - { - $this->session = $session; - } - - /** - * Remember that this list was used so that it can become the default in - * dialog boxes. - * - * @return void - * - * @deprecated Use \VuFind\Favorites\FavoritesService::rememberLastUsedList() - */ - public function rememberLastUsed() - { - if (null !== $this->session) { - $this->session->lastUsed = $this->id; - } - } - - /** - * Given an array of item ids, remove them from all lists. - * - * @param UserEntityInterface|bool $user Logged-in user (false if none) - * @param array $ids IDs to remove from the list - * @param string $source Type of resource identified by IDs - * - * @return void - * - * @deprecated Use \VuFind\Favorites\FavoritesService::removeListResourcesById() - */ - public function removeResourcesById( - $user, - $ids, - $source = DEFAULT_SEARCH_BACKEND - ) { - if (!$this->editAllowed($user ?: null)) { - throw new ListPermissionException('list_access_denied'); - } - - // Retrieve a list of resource IDs: - $resources = $this->getDbService(ResourceServiceInterface::class)->getResourcesByRecordIds($ids, $source); - - $resourceIDs = []; - foreach ($resources as $current) { - $resourceIDs[] = $current->getId(); - } - - // Remove Resource (related tags are also removed implicitly) - $userResourceTable = $this->getDbTable('UserResource'); - $userResourceTable->destroyLinks( - $resourceIDs, - $this->user_id, - $this->id - ); - } - - /** - * Is this a public list? - * - * @return bool - */ - public function isPublic(): bool - { - return isset($this->public) && ($this->public == 1); - } - - /** - * Destroy the list. - * - * @param \VuFind\Db\Row\User|bool $user Logged-in user (false if none) - * @param bool $force Should we force the delete without checking permissions? - * - * @return int The number of rows deleted. - * - * @deprecated Use \VuFind\Favorites\FavoritesService::destroyList() - */ - public function delete($user = false, $force = false) - { - if (!$force && !$this->editAllowed($user ?: null)) { - throw new ListPermissionException('list_access_denied'); - } - - // Remove user_resource and resource_tags rows: - $userResource = $this->getDbTable('UserResource'); - $userResource->destroyLinks(null, $this->user_id, $this->id); - - // Remove resource_tags rows for list tags: - $linker = $this->getDbTable('resourcetags'); - $linker->destroyListLinks($this->id, $user->id); - - // Remove the list itself: - return parent::delete(); - } - - /** - * Get identifier (returns null for an uninitialized or non-persisted object). - * - * @return ?int - */ - public function getId(): ?int - { - return $this->id ?? null; - } - - /** - * Set title. - * - * @param string $title Title - * - * @return static - */ - public function setTitle(string $title): static - { - $this->title = $title; - return $this; - } - - /** - * Get title. - * - * @return string - */ - public function getTitle(): string - { - return $this->title ?? ''; - } - - /** - * Set description. - * - * @param ?string $description Description - * - * @return static - */ - public function setDescription(?string $description): static - { - $this->description = $description; - return $this; - } - - /** - * Get description. - * - * @return ?string - */ - public function getDescription(): ?string - { - return $this->description ?? null; - } - - /** - * Set created date. - * - * @param DateTime $dateTime Created date - * - * @return static - */ - public function setCreated(DateTime $dateTime): static - { - $this->created = $dateTime->format('Y-m-d H:i:s'); - return $this; - } - - /** - * Get created date. - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->created); - } - - /** - * Set whether the list is public. - * - * @param bool $public Is the list public? - * - * @return static - */ - public function setPublic(bool $public): static - { - $this->public = $public ? '1' : '0'; - return $this; - } - - /** - * Set user. - * - * @param ?UserEntityInterface $user User owning the list. - * - * @return static - */ - public function setUser(?UserEntityInterface $user): static - { - $this->user_id = $user?->getId(); - return $this; - } - - /** - * Get user. - * - * @return ?UserEntityInterface - */ - public function getUser(): ?UserEntityInterface - { - return $this->user_id - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->user_id) - : null; - } -} diff --git a/module/VuFind/src/VuFind/Db/Row/UserListFactory.php b/module/VuFind/src/VuFind/Db/Row/UserListFactory.php deleted file mode 100644 index 7103aee37df..00000000000 --- a/module/VuFind/src/VuFind/Db/Row/UserListFactory.php +++ /dev/null @@ -1,78 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Row; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * UserList row gateway factory. - * - * @category VuFind - * @package Db_Row - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class UserListFactory extends RowGatewayFactory -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - $sessionManager = $container->get(\Laminas\Session\SessionManager::class); - $session = new \Laminas\Session\Container('List', $sessionManager); - return parent::__invoke( - $container, - $requestedName, - [$container->get(\VuFind\Tags\TagsService::class), $session] - ); - } -} diff --git a/module/VuFind/src/VuFind/Db/Service/AbstractDbService.php b/module/VuFind/src/VuFind/Db/Service/AbstractDbService.php index 1038e5d0180..4d5f929eb8c 100644 --- a/module/VuFind/src/VuFind/Db/Service/AbstractDbService.php +++ b/module/VuFind/src/VuFind/Db/Service/AbstractDbService.php @@ -29,8 +29,13 @@ namespace VuFind\Db\Service; -use Laminas\Db\RowGateway\AbstractRowGateway; +use Doctrine\ORM\EntityManager; use VuFind\Db\Entity\EntityInterface; +use VuFind\Db\Entity\PluginManager as EntityPluginManager; +use VuFind\Db\PersistenceManager; + +use function is_callable; +use function is_int; /** * Database service abstract base class @@ -51,6 +56,20 @@ abstract class AbstractDbService implements DbServiceInterface */ protected int $retryCount = 5; + /** + * Constructor + * + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager Database entity plugin manager + * @param PersistenceManager $persistenceManager Entity persistence manager + */ + public function __construct( + protected EntityManager $entityManager, + protected EntityPluginManager $entityPluginManager, + protected PersistenceManager $persistenceManager + ) { + } + /** * Persist an entity. * @@ -60,9 +79,72 @@ abstract class AbstractDbService implements DbServiceInterface */ public function persistEntity(EntityInterface $entity): void { - if (!$entity instanceof AbstractRowGateway) { - throw new \Exception('Unexpected entity type'); + $this->persistenceManager->persistEntity($entity); + } + + /** + * Delete an entity. + * + * @param EntityInterface $entity Entity to persist. + * + * @return void + */ + public function deleteEntity(EntityInterface $entity): void + { + $this->persistenceManager->deleteEntity($entity); + } + + /** + * Get a Doctrine reference for an entity or ID. + * + * @param class-string $desiredClass Desired Doctrine entity class + * @param int|EntityInterface $objectOrId Object or identifier to convert to entity + * + * @template T + * + * @return T + */ + public function getDoctrineReference(string $desiredClass, int|EntityInterface $objectOrId): EntityInterface + { + if ($objectOrId instanceof $desiredClass) { + return $objectOrId; + } + if (is_int($objectOrId)) { + $id = $objectOrId; + } else { + if (!is_callable([$objectOrId, 'getId'])) { + throw new \Exception('No getId() method on ' . $objectOrId::class); + } + $id = $objectOrId->getId(); } - $entity->save(); + return $this->entityManager->getReference($desiredClass, $id); + } + + /** + * Retrieve an entity by id. + * + * @param string $entityClass Entity class. + * @param int $id Id of the entity to be retrieved + * + * @return ?object + */ + public function getEntityById($entityClass, $id) + { + return $this->entityManager->find($entityClass, $id); + } + + /** + * Get the row count of a given entity. + * + * @param string $entityClass Entity class. + * + * @return int + */ + public function getRowCountForTable($entityClass) + { + $dql = 'SELECT COUNT(e) FROM ' . $entityClass . ' e '; + $query = $this->entityManager->createQuery($dql); + $count = $query->getSingleScalarResult(); + return $count; } } diff --git a/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php b/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php index ff1563319f4..67ebe27a89d 100644 --- a/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php +++ b/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php @@ -34,6 +34,7 @@ use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface as ContainerException; use Psr\Container\ContainerInterface; +use VuFind\Db\PersistenceManager; /** * Database service factory @@ -65,6 +66,11 @@ public function __invoke( $requestedName, ?array $options = null ) { - return new $requestedName(...($options ?? [])); + return new $requestedName( + $container->get('doctrine.entitymanager.orm_vufind'), + $container->get(\VuFind\Db\Entity\PluginManager::class), + $container->get(PersistenceManager::class), + ...($options ?? []) + ); } } diff --git a/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php b/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php index 7eebfae3786..09dc6871453 100644 --- a/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php +++ b/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php @@ -30,10 +30,8 @@ namespace VuFind\Db\Service; use DateTime; -use Laminas\Log\LoggerAwareInterface; use VuFind\Db\Entity\AccessTokenEntityInterface; -use VuFind\Db\Table\AccessToken; -use VuFind\Log\LoggerAwareTrait; +use VuFind\Db\Entity\User; /** * Database service for access tokens. @@ -46,18 +44,16 @@ */ class AccessTokenService extends AbstractDbService implements AccessTokenServiceInterface, - Feature\DeleteExpiredInterface, - LoggerAwareInterface + Feature\DeleteExpiredInterface { - use LoggerAwareTrait; - /** - * Constructor. + * Create an access_token entity object. * - * @param AccessToken $accessTokenTable Access token table + * @return AccessTokenEntityInterface */ - public function __construct(protected AccessToken $accessTokenTable) + public function createEntity(): AccessTokenEntityInterface { + return $this->entityPluginManager->get(AccessTokenEntityInterface::class); } /** @@ -75,7 +71,22 @@ public function getByIdAndType( string $type, bool $create = true ): ?AccessTokenEntityInterface { - return $this->accessTokenTable->getByIdAndType($id, $type, $create); + $dql = 'SELECT at ' + . 'FROM ' . AccessTokenEntityInterface::class . ' at ' + . 'WHERE at.id = :id ' + . 'AND at.type = :type'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('id', 'type')); + $result = $query->getOneOrNullResult(); + if ($result === null && $create) { + $result = $this->createEntity() + ->setId($id) + ->setType($type) + ->setCreated(new DateTime()); + $this->persistEntity($result); + } + + return $result; } /** @@ -88,7 +99,11 @@ public function getByIdAndType( */ public function storeNonce(int $userId, ?string $nonce): void { - $this->accessTokenTable->storeNonce($userId, $nonce); + $type = 'openid_nonce'; + $token = $this->getByIdAndType((string)$userId, $type); + $token->setUser($this->entityManager->getReference(User::class, $userId)); + $token->setData($nonce); + $this->persistEntity($token); } /** @@ -100,7 +115,9 @@ public function storeNonce(int $userId, ?string $nonce): void */ public function getNonce(int $userId): ?string { - return $this->accessTokenTable->getNonce($userId); + $type = 'openid_nonce'; + $token = $this->getByIdAndType((string)$userId, $type, false); + return $token?->getData(); } /** @@ -113,6 +130,18 @@ public function getNonce(int $userId): ?string */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->accessTokenTable->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('CONCAT(a.id, a.type)') + ->from(AccessTokenEntityInterface::class, 'a') + ->where('a.created < :latestCreated') + ->setParameter('latestCreated', $dateLimit->format('Y-m-d H:i:s')); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(AccessTokenEntityInterface::class, 'a') + ->where('concat(a.id, a.type) IN (:ids)') + ->setParameter('ids', $subQueryBuilder->getQuery()->getResult()); + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceFactory.php b/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceFactory.php deleted file mode 100644 index 1a7d4455e8d..00000000000 --- a/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceFactory.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ - -namespace VuFind\Db\Service; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * Database access token service factory - * - * @category VuFind - * @package Database - * @author Aleksi Peebles - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ -class AccessTokenServiceFactory extends AbstractDbServiceFactory -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - $accessTokenTable = $container->get(\VuFind\Db\Table\PluginManager::class) - ->get('accesstoken'); - return parent::__invoke($container, $requestedName, [$accessTokenTable]); - } -} diff --git a/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceInterface.php b/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceInterface.php index 599ef47c518..cfe3b3c2831 100644 --- a/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceInterface.php +++ b/module/VuFind/src/VuFind/Db/Service/AccessTokenServiceInterface.php @@ -42,6 +42,13 @@ */ interface AccessTokenServiceInterface extends DbServiceInterface { + /** + * Create an access_token entity object. + * + * @return AccessTokenEntityInterface + */ + public function createEntity(): AccessTokenEntityInterface; + /** * Retrieve an object from the database based on id and type; create a new * row if no existing match is found. diff --git a/module/VuFind/src/VuFind/Db/Service/AuthHashService.php b/module/VuFind/src/VuFind/Db/Service/AuthHashService.php index 68d40f93ed9..1fdd3bfec41 100644 --- a/module/VuFind/src/VuFind/Db/Service/AuthHashService.php +++ b/module/VuFind/src/VuFind/Db/Service/AuthHashService.php @@ -31,8 +31,6 @@ use DateTime; use VuFind\Db\Entity\AuthHashEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for auth_hash table. @@ -45,11 +43,8 @@ */ class AuthHashService extends AbstractDbService implements AuthHashServiceInterface, - DbTableAwareInterface, Feature\DeleteExpiredInterface { - use DbTableAwareTrait; - /** * Create an auth_hash entity object. * @@ -57,7 +52,7 @@ class AuthHashService extends AbstractDbService implements */ public function createEntity(): AuthHashEntityInterface { - return $this->getDbTable('AuthHash')->createRow(); + return $this->entityPluginManager->get(AuthHashEntityInterface::class); } /** @@ -69,8 +64,12 @@ public function createEntity(): AuthHashEntityInterface */ public function deleteAuthHash(AuthHashEntityInterface|int $authHashOrId): void { + $dql = 'DELETE FROM ' . AuthHashEntityInterface::class . ' ah ' + . 'WHERE ah.id = :id'; + $query = $this->entityManager->createQuery($dql); $authHashId = $authHashOrId instanceof AuthHashEntityInterface ? $authHashOrId->getId() : $authHashOrId; - $this->getDbTable('AuthHash')->delete(['id' => $authHashId]); + $query->setParameter('id', $authHashId); + $query->execute(); } /** @@ -85,7 +84,22 @@ public function deleteAuthHash(AuthHashEntityInterface|int $authHashOrId): void */ public function getByHashAndType(string $hash, string $type, bool $create = true): ?AuthHashEntityInterface { - return $this->getDbTable('AuthHash')->getByHashAndType($hash, $type, $create); + $dql = 'SELECT ah ' + . 'FROM ' . AuthHashEntityInterface::class . ' ah ' + . 'WHERE ah.hash = :hash ' + . 'AND ah.type = :type'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('hash', 'type')); + $result = $query->getOneOrNullResult(); + if ($result === null && $create) { + $result = $this->createEntity() + ->setHash($hash) + ->setHashType($type) + ->setCreated(new DateTime()); + $this->persistEntity($result); + } + + return $result; } /** @@ -97,7 +111,14 @@ public function getByHashAndType(string $hash, string $type, bool $create = true */ public function getLatestBySessionId(string $sessionId): ?AuthHashEntityInterface { - return $this->getDbTable('AuthHash')->getLatestBySessionId($sessionId); + $dql = 'SELECT ah ' + . 'FROM ' . AuthHashEntityInterface::class . ' ah ' + . 'WHERE ah.sessionId = :sessionId ' + . 'ORDER BY ah.created DESC'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('sessionId', $sessionId); + $result = $query->getOneOrNullResult(); + return $result; } /** @@ -110,6 +131,18 @@ public function getLatestBySessionId(string $sessionId): ?AuthHashEntityInterfac */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->getDbTable('AuthHash')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('ah.id') + ->from(AuthHashEntityInterface::class, 'ah') + ->where('ah.created < :dateLimit') + ->setParameter('dateLimit', $dateLimit->format('Y-m-d H:i:s')); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(AuthHashEntityInterface::class, 'ah') + ->where('ah.id IN (:hashes)') + ->setParameter('hashes', $subQueryBuilder->getQuery()->getResult()); + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ChangeTrackerService.php b/module/VuFind/src/VuFind/Db/Service/ChangeTrackerService.php index 8f5266bfc74..6aa1523445f 100644 --- a/module/VuFind/src/VuFind/Db/Service/ChangeTrackerService.php +++ b/module/VuFind/src/VuFind/Db/Service/ChangeTrackerService.php @@ -31,9 +31,10 @@ namespace VuFind\Db\Service; use DateTime; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\ChangeTracker; use VuFind\Db\Entity\ChangeTrackerEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Log\LoggerAwareTrait; /** * Database service for change tracker. @@ -45,18 +46,9 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class ChangeTrackerService extends AbstractDbService implements - ChangeTrackerServiceInterface, - DbTableAwareInterface +class ChangeTrackerService extends AbstractDbService implements ChangeTrackerServiceInterface, LoggerAwareInterface { - use DbTableAwareTrait; - - /** - * Format to use when sending dates to legacy code. - * - * @var string - */ - protected string $dateFormat = 'Y-m-d H:i:s'; + use LoggerAwareTrait; /** * Retrieve a row from the database based on primary key; return null if it @@ -69,7 +61,15 @@ class ChangeTrackerService extends AbstractDbService implements */ public function getChangeTrackerEntity(string $indexName, string $id): ?ChangeTrackerEntityInterface { - return $this->getDbTable('ChangeTracker')->retrieve($indexName, $id); + $dql = 'SELECT c ' + . 'FROM ' . ChangeTrackerEntityInterface::class . ' c ' + . 'WHERE c.core = :core AND c.id = :id'; + $parameters = ['core' => $indexName, 'id' => $id]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $queryResult = $query->getResult(); + $result = current($queryResult); + return $result ? $result : null; } /** @@ -83,11 +83,14 @@ public function getChangeTrackerEntity(string $indexName, string $id): ?ChangeTr */ public function getDeletedCount(string $indexName, DateTime $from, DateTime $until): int { - return $this->getDbTable('ChangeTracker')->retrieveDeletedCount( - $indexName, - $from->format($this->dateFormat), - $until->format($this->dateFormat) - ); + $dql = 'SELECT COUNT(c) as deletedcount ' + . 'FROM ' . ChangeTrackerEntityInterface::class . ' c ' + . 'WHERE c.core = :core AND c.deleted BETWEEN :from AND :until'; + $parameters = ['core' => $indexName, 'from' => $from, 'until' => $until]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->getResult(); + return current($result)['deletedcount']; } /** @@ -108,15 +111,43 @@ public function getDeletedEntities( int $offset = 0, ?int $limit = null ): array { - return iterator_to_array( - $this->getDbTable('ChangeTracker')->retrieveDeleted( - $indexName, - $from->format($this->dateFormat), - $until->format($this->dateFormat), - $offset, - $limit - ) - ); + $dql = 'SELECT c ' + . 'FROM ' . ChangeTrackerEntityInterface::class . ' c ' + . 'WHERE c.core = :core AND c.deleted BETWEEN :from AND :until ' + . 'ORDER BY c.deleted'; + $parameters = ['core' => $indexName, 'from' => $from, 'until' => $until]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->setFirstResult($offset); + if (null !== $limit) { + $query->setMaxResults($limit); + } + $result = $query->getResult(); + return $result; + } + + /** + * Retrieve a row from the database based on primary key; create a new + * row if no existing match is found. + * + * @param string $core The Solr core holding the record. + * @param string $id The ID of the record being indexed. + * + * @return ChangeTracker + */ + protected function retrieveOrCreate(string $core, string $id): ChangeTracker + { + $row = $this->getChangeTrackerEntity($core, $id); + if (empty($row)) { + $now = new \DateTime(); + $row = $this->createEntity() + ->setIndexName($core) + ->setId($id) + ->setFirstIndexed($now) + ->setLastIndexed($now); + $this->persistEntity($row); + } + return $row; } /** @@ -131,7 +162,18 @@ public function getDeletedEntities( */ public function markDeleted(string $core, string $id): ChangeTrackerEntityInterface { - return $this->getDbTable('ChangeTracker')->markDeleted($core, $id); + // Get a row matching the specified details: + $row = $this->retrieveOrCreate($core, $id); + + // If the record is already deleted, we don't need to do anything! + if (!empty($row->getDeleted())) { + return $row; + } + + // Save new value to the object: + $row->setDeleted(new \DateTime()); + $this->persistEntity($row); + return $row; } /** @@ -150,6 +192,85 @@ public function markDeleted(string $core, string $id): ChangeTrackerEntityInterf */ public function index(string $core, string $id, int $change): ChangeTrackerEntityInterface { - return $this->getDbTable('ChangeTracker')->index($core, $id, $change); + // Get a row matching the specified details: + $row = $this->retrieveOrCreate($core, $id); + + // Flag to indicate whether we need to save the contents of $row: + $saveNeeded = false; + $utcTime = \DateTime::createFromFormat('U', $change); + + // Make sure there is a change date in the row (this will be empty + // if we just created a new row): + if (empty($row->getLastRecordChange())) { + $row->setLastRecordChange($utcTime); + $saveNeeded = true; + } + + // Are we restoring a previously deleted record, or was the stored + // record change date before current record change date? Either way, + // we need to update the table! + if (!empty($row->getDeleted()) || $row->getLastRecordChange() < $utcTime) { + // Save new values to the object: + $now = new \DateTime(); + $row->setLastIndexed($now); + $row->setLastRecordChange($utcTime); + + // If first indexed is null, we're restoring a deleted record, so + // we need to treat it as new -- we'll use the current time. + if (empty($row->getFirstIndexed())) { + $row->setFirstIndexed($now); + } + + // Make sure the record is "undeleted" if necessary: + $row->setDeleted(null); + + $saveNeeded = true; + } + + // Save the row if changes were made: + if ($saveNeeded) { + $this->persistEntity($row); + } + + // Send back the row: + return $row; + } + + /** + * Remove all or selected rows from the database. + * + * @param ?string $core The Solr core holding the record. + * @param ?string $id The ID of the record being indexed. + * + * @return void + */ + public function deleteRows(?string $core = null, ?string $id = null): void + { + $dql = 'DELETE FROM ' . ChangeTrackerEntityInterface::class . ' c '; + $parameters = $dqlWhere = []; + if (null !== $core) { + $dqlWhere[] = 'c.core = :core'; + $parameters['core'] = $core; + } + if (null !== $id) { + $dqlWhere[] = 'c.id = :id'; + $parameters['id'] = $id; + } + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); + } + + /** + * Create a change tracker entity object. + * + * @return ChangeTrackerEntityInterface + */ + public function createEntity(): ChangeTrackerEntityInterface + { + return $this->entityPluginManager->get(ChangeTrackerEntityInterface::class); } } diff --git a/module/VuFind/src/VuFind/Db/Service/CommentsService.php b/module/VuFind/src/VuFind/Db/Service/CommentsService.php index ee07597d213..0bc4e3f72da 100644 --- a/module/VuFind/src/VuFind/Db/Service/CommentsService.php +++ b/module/VuFind/src/VuFind/Db/Service/CommentsService.php @@ -29,13 +29,15 @@ namespace VuFind\Db\Service; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; +use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrinePaginatorAdapter; +use Laminas\Log\LoggerAwareInterface; +use Laminas\Paginator\Paginator; use VuFind\Db\Entity\CommentsEntityInterface; use VuFind\Db\Entity\ResourceEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Log\LoggerAwareTrait; -use function is_array; use function is_int; /** @@ -50,10 +52,10 @@ class CommentsService extends AbstractDbService implements CommentsServiceInterface, DbServiceAwareInterface, - DbTableAwareInterface + LoggerAwareInterface { use DbServiceAwareTrait; - use DbTableAwareTrait; + use LoggerAwareTrait; /** * Create a comments entity object. @@ -62,7 +64,7 @@ class CommentsService extends AbstractDbService implements */ public function createEntity(): CommentsEntityInterface { - return $this->getDbTable('comments')->createRow(); + return $this->entityPluginManager->get(CommentsEntityInterface::class); } /** @@ -79,13 +81,20 @@ public function addComment( UserEntityInterface|int $userOrId, ResourceEntityInterface|int $resourceOrId ): ?int { - $user = is_int($userOrId) - ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) - : $userOrId; - $resource = is_int($resourceOrId) - ? $this->getDbService(ResourceServiceInterface::class)->getResourceById($resourceOrId) - : $resourceOrId; - return $resource->addComment($comment, $user); + $data = $this->createEntity() + ->setUser($this->getDoctrineReference(UserEntityInterface::class, $userOrId)) + ->setComment($comment) + ->setCreated(new \DateTime()) + ->setResource($this->getDoctrineReference(ResourceEntityInterface::class, $resourceOrId)); + + try { + $this->persistEntity($data); + } catch (\Exception $e) { + $this->logError('Could not save comment: ' . $e->getMessage()); + return null; + } + + return $data->getId(); } /** @@ -98,8 +107,22 @@ public function addComment( */ public function getRecordComments(string $id, string $source = DEFAULT_SEARCH_BACKEND): array { - $comments = $this->getDbTable('comments')->getForResource($id, $source); - return is_array($comments) ? $comments : iterator_to_array($comments); + $resourceService = $this->getDbService(ResourceServiceInterface::class); + $resource = $resourceService->getResourceByRecordId($id, $source); + if (!$resource) { + return []; + } + $dql = 'SELECT c ' + . 'FROM ' . CommentsEntityInterface::class . ' c ' + . 'LEFT JOIN c.user u ' + . 'WHERE c.resource = :resource ' + . 'ORDER BY c.created ASC'; + + $parameters = compact('resource'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->getResult(); + return $result; } /** @@ -112,9 +135,22 @@ public function getRecordComments(string $id, string $source = DEFAULT_SEARCH_BA */ public function deleteIfOwnedByUser(int $id, UserEntityInterface|int $userOrId): bool { - $user = is_int($userOrId) - ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId; - return $this->getDbTable('comments')->deleteIfOwnedByUser($id, $user); + if (null === $userOrId) { + return false; + } + + $userId = is_int($userOrId) ? $userOrId : $userOrId->getId(); + $comment = $this->getCommentById($id); + if ($userId !== $comment->getUser()->getId()) { + return false; + } + + $del = 'DELETE FROM ' . CommentsEntityInterface::class . ' c ' + . 'WHERE c.id = :id AND c.user = :user'; + $query = $this->entityManager->createQuery($del); + $query->setParameters(['id' => $id, 'user' => $userId]); + $query->execute(); + return true; } /** @@ -126,9 +162,11 @@ public function deleteIfOwnedByUser(int $id, UserEntityInterface|int $userOrId): */ public function deleteByUser(UserEntityInterface|int $userOrId): void { - $user = is_int($userOrId) - ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId; - $this->getDbTable('comments')->deleteByUser($user); + $dql = 'DELETE FROM ' . CommentsEntityInterface::class . ' c ' + . 'WHERE c.user = :user'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(['user' => is_int($userOrId) ? $userOrId : $userOrId->getId()]); + $query->execute(); } /** @@ -138,7 +176,13 @@ public function deleteByUser(UserEntityInterface|int $userOrId): void */ public function getStatistics(): array { - return $this->getDbTable('comments')->getStatistics(); + $dql = 'SELECT COUNT(DISTINCT(c.user)) AS users, ' + . 'COUNT(DISTINCT(c.resource)) AS resources, ' + . 'COUNT(c.id) AS total ' + . 'FROM ' . CommentsEntityInterface::class . ' c'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -150,7 +194,7 @@ public function getStatistics(): array */ public function getCommentById(int $id): ?CommentsEntityInterface { - return $this->getDbTable('comments')->select(['id' => $id])->current(); + return $this->entityManager->find(CommentsEntityInterface::class, $id); } /** @@ -163,13 +207,18 @@ public function getCommentById(int $id): ?CommentsEntityInterface */ public function changeResourceId(int $old, int $new): void { - $this->getDbTable('comments')->update(['resource_id' => $new], ['resource_id' => $old]); + $dql = 'UPDATE ' . CommentsEntityInterface::class . ' e ' + . 'SET e.resource = :new WHERE e.resource = :old'; + $parameters = compact('new', 'old'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** - * Get a paginated result of all comments by user id. + * Get a paginated result of all comments made by the user. * - * @param int $userId User Id + * @param int $userId User ID * @param int $limit Limit * @param int $page Page * @param string $sort Sort @@ -180,14 +229,35 @@ public function getCommentsPaginator( int $userId, int $limit, int $page, - string $sort, - ): \Laminas\Paginator\Paginator { - return $this->getDbTable('Comments')->getCommentsPaginator( - $userId, - $limit, - $page, - $sort, - ); + string $sort + ): Paginator { + $dql = 'SELECT c.id, c.comment, c.created AS created, ' + . 'u.id AS user_id, u.username AS username, ' + . 'r.id AS resource_id, r.recordId AS record_id, r.source AS source, r.title AS title ' + . 'FROM ' . CommentsEntityInterface::class . ' c ' + . 'LEFT JOIN c.user u ' + . 'LEFT JOIN c.resource r ' + . 'WHERE c.user = :userId'; + + $parameters = ['userId' => $userId]; + + $sortOrder = $sort ? $sort : 'created DESC'; + + $dql .= ' ORDER BY ' . $sortOrder; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit); + + $doctrinePaginator = new DoctrinePaginator($query); + $doctrinePaginator->setUseOutputWalkers(false); + + $paginator = new Paginator(new DoctrinePaginatorAdapter($doctrinePaginator)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + + return $paginator; } /** @@ -200,10 +270,14 @@ public function getCommentsPaginator( */ public function deleteByIdsAndUserId(array $ids, int $userId): void { - $callback = function ($select) use ($ids, $userId) { - $select->where->in('id', $ids); - $select->where->equalTo('user_id', $userId); - }; - $this->getDbTable('Comments')->delete($callback); + $dql = 'DELETE FROM ' . CommentsEntityInterface::class . ' c ' + . 'WHERE c.user = :user AND c.id IN (:ids)'; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters([ + 'user' => $userId, + 'ids' => $ids, + ]); + $query->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ExternalSessionService.php b/module/VuFind/src/VuFind/Db/Service/ExternalSessionService.php index 88f29186900..5c35c62f5c9 100644 --- a/module/VuFind/src/VuFind/Db/Service/ExternalSessionService.php +++ b/module/VuFind/src/VuFind/Db/Service/ExternalSessionService.php @@ -31,8 +31,6 @@ use DateTime; use VuFind\Db\Entity\ExternalSessionEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for external_session table. @@ -45,11 +43,8 @@ */ class ExternalSessionService extends AbstractDbService implements ExternalSessionServiceInterface, - Feature\DeleteExpiredInterface, - DbTableAwareInterface + Feature\DeleteExpiredInterface { - use DbTableAwareTrait; - /** * Create a new external session entity. * @@ -57,7 +52,7 @@ class ExternalSessionService extends AbstractDbService implements */ public function createEntity(): ExternalSessionEntityInterface { - return $this->getDbTable('ExternalSession')->createRow(); + return $this->entityPluginManager->get(ExternalSessionEntityInterface::class); } /** @@ -90,7 +85,13 @@ public function addSessionMapping( */ public function getAllByExternalSessionId(string $sid): array { - return iterator_to_array($this->getDbTable('ExternalSession')->select(['external_session_id' => $sid])); + $dql = 'SELECT es ' + . 'FROM ' . ExternalSessionEntityInterface::class . ' es ' + . 'WHERE es.externalSessionId = :esid '; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('esid', $sid); + $result = $query->getResult(); + return $result; } /** @@ -102,7 +103,11 @@ public function getAllByExternalSessionId(string $sid): array */ public function destroySession(string $sid): void { - $this->getDbTable('ExternalSession')->delete(['session_id' => $sid]); + $dql = 'DELETE FROM ' . ExternalSessionEntityInterface::class . ' es' + . ' WHERE es.sessionId = :sid'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('sid', $sid); + $query->execute(); } /** @@ -115,6 +120,18 @@ public function destroySession(string $sid): void */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->getDbTable('ExternalSession')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('es.id') + ->from(ExternalSessionEntityInterface::class, 'es') + ->where('es.created < :dateLimit') + ->setParameter('dateLimit', $dateLimit); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(ExternalSessionEntityInterface::class, 'es') + ->where('es.id IN (:ids)') + ->setParameter('ids', $subQueryBuilder->getQuery()->getResult()); + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/Feature/ResourceSortTrait.php b/module/VuFind/src/VuFind/Db/Service/Feature/ResourceSortTrait.php new file mode 100644 index 00000000000..e22e006fcb3 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Service/Feature/ResourceSortTrait.php @@ -0,0 +1,96 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ + +namespace VuFind\Db\Service\Feature; + +use function in_array; + +/** + * Trait for sorting results from the resource table. + * + * @category VuFind + * @package Database + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki + */ +trait ResourceSortTrait +{ + /** + * Apply a sort parameter to a query on the resource table. Returns an + * array with two keys: 'orderByClause' (the actual ORDER BY) and + * 'extraSelect' (extra values to add to SELECT, if necessary) + * + * @param string $sort Field to use for sorting (may include + * 'desc' qualifier) + * @param string $alias Alias to the resource table (defaults to 'r') + * + * @return array + */ + protected function getResourceOrderByClause(string $sort, string $alias = 'r'): array + { + // Apply sorting, if necessary: + $legalSorts = [ + 'title', 'title desc', 'author', 'author desc', 'year', 'year desc', 'last_saved', 'last_saved desc', + ]; + $orderByClause = $extraSelect = ''; + if (!empty($sort) && in_array(strtolower($sort), $legalSorts)) { + // Strip off 'desc' to obtain the raw field name -- we'll need it + // to sort null values to the bottom: + $parts = explode(' ', $sort); + $rawField = trim($parts[0]); + + // Start building the list of sort fields: + $order = []; + + // Only include the table alias on non-virtual fields: + $fieldPrefix = (strtolower($rawField) === 'last_saved') ? '' : "$alias."; + + // The title field can't be null, so don't bother with the extra + // isnull() sort in that case. + if (strtolower($rawField) === 'title') { + // Do nothing + } elseif (strtolower($rawField) === 'last_saved') { + $extraSelect = 'ur.saved AS HIDDEN last_saved, ' + . 'CASE WHEN ur.saved IS NULL THEN 1 ELSE 0 END AS HIDDEN last_savedsort'; + $order[] = 'last_savedsort'; + } else { + $extraSelect = 'CASE WHEN ' . $fieldPrefix . $rawField . ' IS NULL THEN 1 ELSE 0 END AS HIDDEN ' + . $rawField . 'sort'; + $order[] = "{$rawField}sort"; + } + + // Apply the user-specified sort: + $order[] = $fieldPrefix . $sort; + // Inject the sort preferences into the query object: + $orderByClause = ' ORDER BY ' . implode(', ', $order); + } + return compact('orderByClause', 'extraSelect'); + } +} diff --git a/module/VuFind/src/VuFind/Db/Service/FeedbackService.php b/module/VuFind/src/VuFind/Db/Service/FeedbackService.php index 038d953946a..4311b6e78bd 100644 --- a/module/VuFind/src/VuFind/Db/Service/FeedbackService.php +++ b/module/VuFind/src/VuFind/Db/Service/FeedbackService.php @@ -23,33 +23,44 @@ * @category VuFind * @package Database * @author Sudharma Kellampalli - * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ namespace VuFind\Db\Service; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; +use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrinePaginatorAdapter; use Laminas\Paginator\Paginator; +use VuFind\Db\Entity\Feedback; use VuFind\Db\Entity\FeedbackEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; use function count; +use function intval; /** * Database service for feedback. * * @category VuFind * @package Database - * @author Sudharma Kellampalli * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class FeedbackService extends AbstractDbService implements DbTableAwareInterface, FeedbackServiceInterface +class FeedbackService extends AbstractDbService implements FeedbackServiceInterface { - use DbTableAwareTrait; + /** + * Db column name to Doctrine entity field mapper + * + * @var array + */ + protected $fieldMap = [ + 'form_data' => 'formData', + 'form_name' => 'formName', + 'site_url' => 'siteUrl', + 'user_id' => 'user', + 'updated_by' => 'updatedBy', + ]; /** * Create a feedback entity object. @@ -58,7 +69,7 @@ class FeedbackService extends AbstractDbService implements DbTableAwareInterface */ public function createEntity(): FeedbackEntityInterface { - return $this->getDbTable('feedback')->createRow(); + return $this->entityPluginManager->get(FeedbackEntityInterface::class); } /** @@ -70,7 +81,7 @@ public function createEntity(): FeedbackEntityInterface */ public function getFeedbackById(int $id): ?FeedbackEntityInterface { - return $this->getDbTable('feedback')->select(['id' => $id])->current(); + return $this->entityManager->find(FeedbackEntityInterface::class, $id); } /** @@ -91,25 +102,39 @@ public function getFeedbackPaginator( ?int $page = null, int $limit = 20 ): Paginator { - // The template expects a different format than what is returned by Laminas\Db; we need to do - // some data conversion and then populate a new paginator with the remapped results. We'll use - // a padded array and the array adapter to make this work. Probably not the most robust solution, - // but good enough for the current needs of the software; this will go away in a future database - // layer migration. - $feedbackTable = $this->getDbTable('feedback'); - $paginator = $feedbackTable->getFeedbackByFilter($formName, $siteUrl, $status, $page, $limit); - $results = array_fill(0, count($paginator->getAdapter()), []); - $index = (($page ?? 1) - 1) * $limit; - foreach ($paginator as $current) { - $row = (array)$current; - $row['feedback_entity'] = $feedbackTable->createRow()->populate($row); - $results[$index] = $row; - $index++; + $dql = 'SELECT f AS feedback_entity FROM ' . FeedbackEntityInterface::class . ' f'; + $parameters = $dqlWhere = []; + + if (null !== $formName) { + $dqlWhere[] = 'f.formName = :formName'; + $parameters['formName'] = $formName; + } + if (null !== $siteUrl) { + $dqlWhere[] = 'f.siteUrl = :siteUrl'; + $parameters['siteUrl'] = $siteUrl; + } + if (null !== $status) { + $dqlWhere[] = 'f.status = :status'; + $parameters['status'] = $status; } - $newPaginator = new Paginator(new \Laminas\Paginator\Adapter\ArrayAdapter($results)); - $newPaginator->setCurrentPageNumber($page ?? 1); - $newPaginator->setItemCountPerPage($limit); - return $newPaginator; + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $dql .= ' ORDER BY f.created DESC'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + + $page = null === $page ? null : intval($page); + if (null !== $page) { + $query->setMaxResults($limit); + $query->setFirstResult($limit * ($page - 1)); + } + $paginator = new Paginator(new DoctrinePaginatorAdapter(new DoctrinePaginator($query))); + if (null !== $page) { + $paginator->setCurrentPageNumber($page); + $paginator->setItemCountPerPage($limit); + } + return $paginator; } /** @@ -121,7 +146,44 @@ public function getFeedbackPaginator( */ public function deleteByIdArray(array $ids): int { - return $this->getDbTable('feedback')->deleteByIdArray($ids); + // Do nothing if we have no IDs to delete! + if (empty($ids)) { + return 0; + } + $dql = 'DELETE FROM ' . FeedbackEntityInterface::class . ' fb ' + . 'WHERE fb.id IN (:ids)'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('ids')); + $query->execute(); + return count($ids); + } + + /** + * Get values for a column + * + * @param string $column Column name + * + * @return array + */ + public function getColumn(string $column): array + { + $dql = 'SELECT f.id, f.' . $this->mapField($column) + . ' FROM ' . FeedbackEntityInterface::class . ' f ' + . 'ORDER BY f.' . $this->mapField($column); + $query = $this->entityManager->createQuery($dql); + return $query->getResult(); + } + + /** + * Column mapper + * + * @param string $column Column name + * + * @return string + */ + protected function mapField($column) + { + return $this->fieldMap[$column] ?? $column; } /** @@ -133,14 +195,6 @@ public function deleteByIdArray(array $ids): int */ public function getUniqueColumn(string $column): array { - $feedbackTable = $this->getDbTable('feedback'); - $feedback = $feedbackTable->select( - function ($select) use ($column) { - $select->columns(['id', $column]); - $select->order($column); - } - ); - $feedbackArray = $feedback->toArray(); - return array_unique(array_column($feedbackArray, $column)); + return array_unique(array_column($this->getColumn($column), $this->mapField($column))); } } diff --git a/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php b/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php index d75b6018b12..680ca6200df 100644 --- a/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php +++ b/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php @@ -32,11 +32,8 @@ use DateTime; use VuFind\Db\Entity\LoginTokenEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; use VuFind\Exception\LoginToken as LoginTokenException; -use function is_int; - /** * Database service for login_token table. * @@ -48,11 +45,8 @@ */ class LoginTokenService extends AbstractDbService implements LoginTokenServiceInterface, - Feature\DeleteExpiredInterface, - DbTableAwareInterface + Feature\DeleteExpiredInterface { - use \VuFind\Db\Table\DbTableAwareTrait; - /** * Create a new login token entity. * @@ -60,7 +54,7 @@ class LoginTokenService extends AbstractDbService implements */ public function createEntity(): LoginTokenEntityInterface { - return $this->getDbTable('LoginToken')->createRow(); + return $this->entityPluginManager->get(LoginTokenEntityInterface::class); } /** @@ -108,7 +102,37 @@ public function createAndPersistToken( */ public function matchToken(array $token): ?LoginTokenEntityInterface { - return $this->getDbTable('LoginToken')->matchToken($token); + $userId = null; + foreach ($this->getBySeries($token['series']) as $row) { + $userId = $row->getUser()->getId(); + if (hash_equals($row->getToken(), hash('sha256', $token['token']))) { + if (time() > $row->getExpires()) { + $this->deleteById($row->getId()); + return null; + } + return $row; + } + } + if ($userId) { + throw new LoginTokenException('Tokens do not match', $userId); + } + return null; + } + + /** + * Delete a token with given id. + * + * @param int $id id + * + * @return void + */ + protected function deleteById(int $id): void + { + $dql = 'DELETE FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.id == :id'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('id', $id); + $query->execute(); } /** @@ -121,7 +145,16 @@ public function matchToken(array $token): ?LoginTokenEntityInterface */ public function deleteBySeries(string $series, ?int $currentTokenId = null): void { - $this->getDbTable('LoginToken')->deleteBySeries($series, $currentTokenId); + $params = compact('series'); + $dql = 'DELETE FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.series = :series'; + if ($currentTokenId !== null) { + $dql .= ' AND lt.id != :currentTokenId'; + $params['currentTokenId'] = $currentTokenId; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($params); + $query->execute(); } /** @@ -133,8 +166,12 @@ public function deleteBySeries(string $series, ?int $currentTokenId = null): voi */ public function deleteByUser(UserEntityInterface|int $userOrId): void { - $userId = is_int($userOrId) ? $userOrId : $userOrId->getId(); - $this->getDbTable('LoginToken')->deleteByUserId($userId); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $dql = 'DELETE FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.user = :user'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('user', $user); + $query->execute(); } /** @@ -147,8 +184,29 @@ public function deleteByUser(UserEntityInterface|int $userOrId): void */ public function getByUser(UserEntityInterface|int $userOrId, bool $grouped = true): array { - $userId = is_int($userOrId) ? $userOrId : $userOrId->getId(); - return $this->getDbTable('LoginToken')->getByUserId($userId, $grouped); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + if ($grouped) { + // Use different DQL for grouping logic + $dql = 'SELECT lt ' + . 'FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.user = :user AND lt.lastLogin = (' + . ' SELECT MAX(subLt.lastLogin) ' + . ' FROM ' . LoginTokenEntityInterface::class . ' subLt ' + . ' WHERE subLt.user = :user AND subLt.series = lt.series AND subLt.browser = lt.browser ' + . ' AND subLt.platform = lt.platform AND subLt.expires = lt.expires ' + . ') ' + . 'ORDER BY lt.lastLogin DESC'; + } else { + $dql = 'SELECT lt ' + . 'FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.user = :user ' + . 'ORDER BY lt.lastLogin DESC'; + } + + $query = $this->entityManager->createQuery($dql); + $query->setParameter('user', $user); + $result = $query->getResult(); + return $result; } /** @@ -160,7 +218,13 @@ public function getByUser(UserEntityInterface|int $userOrId, bool $grouped = tru */ public function getBySeries(string $series): array { - return iterator_to_array($this->getDbTable('LoginToken')->getBySeries($series)); + $dql = 'SELECT lt ' + . 'FROM ' . LoginTokenEntityInterface::class . ' lt ' + . 'WHERE lt.series = :series'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('series', $series); + $result = $query->getResult(); + return $result; } /** @@ -173,6 +237,19 @@ public function getBySeries(string $series): array */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->getDbTable('LoginToken')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + // Date limit ignored since login token already contains an expiration time. + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('lt.id') + ->from(LoginTokenEntityInterface::class, 'lt') + ->where('lt.expires < :dateLimit') + ->setParameter('dateLimit', $dateLimit->getTimestamp()); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(LoginTokenEntityInterface::class, 'lt') + ->where('lt.id IN (:tokens)') + ->setParameter('tokens', $subQueryBuilder->getQuery()->getResult()); + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/OaiResumptionService.php b/module/VuFind/src/VuFind/Db/Service/OaiResumptionService.php index 6f8f83212ee..11b8ca555c1 100644 --- a/module/VuFind/src/VuFind/Db/Service/OaiResumptionService.php +++ b/module/VuFind/src/VuFind/Db/Service/OaiResumptionService.php @@ -30,9 +30,8 @@ namespace VuFind\Db\Service; use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\OaiResumption; use VuFind\Db\Entity\OaiResumptionEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; use VuFind\Log\LoggerAwareTrait; use function intval; @@ -47,11 +46,9 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class OaiResumptionService extends AbstractDbService implements - DbTableAwareInterface, LoggerAwareInterface, OaiResumptionServiceInterface { - use DbTableAwareTrait; use LoggerAwareTrait; /** @@ -61,7 +58,12 @@ class OaiResumptionService extends AbstractDbService implements */ public function removeExpired(): void { - $this->getDbTable('oairesumption')->removeExpired(); + $dql = 'DELETE FROM ' . OaiResumptionEntityInterface::class . ' O ' + . 'WHERE O.expires <= :now'; + $parameters['now'] = new \DateTime(); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -73,7 +75,7 @@ public function removeExpired(): void * @return ?OaiResumptionEntityInterface * @deprecated Use OaiResumptionService::findWithId */ - public function findToken(string $token): ?OaiResumptionEntityInterface + public function findToken($token): ?OaiResumptionEntityInterface { return $this->findWithId($token); } @@ -88,7 +90,12 @@ public function findToken(string $token): ?OaiResumptionEntityInterface */ public function findWithId(string $id): ?OaiResumptionEntityInterface { - return $this->getDbTable('oairesumption')->findWithId($id); + $dql = 'SELECT O FROM ' . OaiResumptionEntityInterface::class . ' O ' + . 'WHERE O.id = :id'; + $parameters = compact('id'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getOneOrNullResult(); } /** @@ -101,7 +108,30 @@ public function findWithId(string $id): ?OaiResumptionEntityInterface */ public function findWithToken(string $token): ?OaiResumptionEntityInterface { - return $this->getDbTable('oairesumption')->findWithToken($token); + $dql = 'SELECT O FROM ' . OaiResumptionEntityInterface::class . ' O ' + . 'WHERE O.token = :token'; + $parameters = compact('token'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getOneOrNullResult(); + } + + /** + * Retrieve a row from the database based on primary key and where the token is null. + * + * @param int $id Id used for the search. + * + * @return ?OaiResumptionEntityInterface + * @todo In future, we should migrate data to prevent null token fields, which will make this method obsolete. + */ + protected function findWithLegacyIdToken(int $id): ?OaiResumptionEntityInterface + { + $dql = 'SELECT O FROM ' . OaiResumptionEntityInterface::class . ' O ' + . 'WHERE O.token IS NULL AND O.id = :id'; + $parameters = compact('id'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getOneOrNullResult(); } /** @@ -118,7 +148,7 @@ final public function findWithTokenOrLegacyIdToken(string $tokenOrId): ?OaiResum if (!$result && is_numeric($tokenOrId)) { $idInt = intval($tokenOrId); if ($idInt > 0) { - $result = $this->getDbTable('oairesumption')->findWithLegacyIdToken($idInt); + return $this->findWithLegacyIdToken($idInt); } } return $result; @@ -174,7 +204,7 @@ public function createAndPersistToken(array $params, int $expire): OaiResumption */ public function createEntity(): OaiResumptionEntityInterface { - return $this->getDbTable('oairesumption')->createRow(); + return $this->entityPluginManager->get(OaiResumptionEntityInterface::class); } /** diff --git a/module/VuFind/src/VuFind/Db/Service/PluginManager.php b/module/VuFind/src/VuFind/Db/Service/PluginManager.php index 17c01970bb2..2b5fa0578bd 100644 --- a/module/VuFind/src/VuFind/Db/Service/PluginManager.php +++ b/module/VuFind/src/VuFind/Db/Service/PluginManager.php @@ -77,7 +77,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @var array */ protected $factories = [ - AccessTokenService::class => AccessTokenServiceFactory::class, + AccessTokenService::class => AbstractDbServiceFactory::class, AuthHashService::class => AbstractDbServiceFactory::class, ChangeTrackerService::class => AbstractDbServiceFactory::class, CommentsService::class => AbstractDbServiceFactory::class, diff --git a/module/VuFind/src/VuFind/Db/Service/RatingsService.php b/module/VuFind/src/VuFind/Db/Service/RatingsService.php index 38b096fc2c7..6fc878c916d 100644 --- a/module/VuFind/src/VuFind/Db/Service/RatingsService.php +++ b/module/VuFind/src/VuFind/Db/Service/RatingsService.php @@ -29,10 +29,14 @@ namespace VuFind\Db\Service; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; +use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrinePaginatorAdapter; +use Laminas\Log\LoggerAwareInterface; +use Laminas\Paginator\Paginator; +use VuFind\Db\Entity\RatingsEntityInterface; use VuFind\Db\Entity\ResourceEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Log\LoggerAwareTrait; use function is_int; @@ -46,10 +50,12 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class RatingsService extends AbstractDbService implements - DbTableAwareInterface, + DbServiceAwareInterface, + LoggerAwareInterface, RatingsServiceInterface { - use DbTableAwareTrait; + use DbServiceAwareTrait; + use LoggerAwareTrait; /** * Get average rating and rating count associated with the specified record. @@ -62,7 +68,31 @@ class RatingsService extends AbstractDbService implements */ public function getRecordRatings(string $id, string $source, ?int $userId): array { - return $this->getDbTable('ratings')->getForResource($id, $source, $userId); + $resourceService = $this->getDbService(ResourceServiceInterface::class); + $resource = $resourceService->getResourceByRecordId($id, $source); + if (!$resource) { + return [ + 'count' => 0, + 'rating' => 0, + ]; + } + $dql = 'SELECT COUNT(r.id) AS count, AVG(r.rating) AS rating ' + . 'FROM ' . RatingsEntityInterface::class . ' r '; + + $dqlWhere[] = 'r.resource = :resource'; + $parameters['resource'] = $resource; + if (null !== $userId) { + $dqlWhere[] = 'r.user = :user'; + $parameters['user'] = $userId; + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->getResult(); + return [ + 'count' => $result[0]['count'], + 'rating' => floor($result[0]['rating'] ?? 0) ?? 0, + ]; } /** @@ -80,7 +110,52 @@ public function getCountsForRecord( string $source, array $groups ): array { - return $this->getDbTable('ratings')->getCountsForResource($id, $source, $groups); + $result = [ + 'count' => 0, + 'rating' => 0, + 'groups' => [], + ]; + foreach (array_keys($groups) as $key) { + $result['groups'][$key] = 0; + } + + $resourceService = $this->getDbService(ResourceServiceInterface::class); + $resource = $resourceService->getResourceByRecordId($id, $source); + if (!$resource) { + return $result; + } + $dql = 'SELECT COUNT(r.id) AS count, r.rating AS rating ' + . 'FROM ' . RatingsEntityInterface::class . ' r ' + . 'WHERE r.resource = :resource ' + . 'GROUP BY rating'; + + $parameters['resource'] = $resource; + + $query = $this->entityManager->createQuery($dql); + + $query->setParameters($parameters); + $queryResult = $query->getResult(); + + $ratingTotal = 0; + $groupCount = 0; + foreach ($queryResult as $rating) { + $result['count'] += $rating['count']; + $ratingTotal += $rating['rating']; + ++$groupCount; + if ($groups) { + foreach ($groups as $key => $range) { + if ( + $rating['rating'] >= $range[0] + && $rating['rating'] <= $range[1] + ) { + $result['groups'][$key] = ($result['groups'][$key] ?? 0) + + $rating['count']; + } + } + } + } + $result['rating'] = $groupCount ? floor($ratingTotal / $groupCount) : 0; + return $result; } /** @@ -92,9 +167,12 @@ public function getCountsForRecord( */ public function deleteByUser(UserEntityInterface|int $userOrId): void { - $this->getDbTable('ratings')->deleteByUser( - is_int($userOrId) ? $this->getDbTable('user')->getById($userOrId) : $userOrId - ); + $dql = 'DELETE FROM ' . RatingsEntityInterface::class . ' r ' + . 'WHERE r.user = :user'; + $parameters['user'] = is_int($userOrId) ? $userOrId : $userOrId->getId(); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -104,7 +182,13 @@ public function deleteByUser(UserEntityInterface|int $userOrId): void */ public function getStatistics(): array { - return $this->getDbTable('ratings')->getStatistics(); + $dql = 'SELECT COUNT(DISTINCT(r.user)) AS users, ' + . 'COUNT(DISTINCT(r.resource)) AS resources, ' + . 'COUNT(r.id) AS total ' + . 'FROM ' . RatingsEntityInterface::class . ' r'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -122,9 +206,61 @@ public function addOrUpdateRating( UserEntityInterface|int $userOrId, ?int $rating ): int { - $resource = is_int($resourceOrId) - ? $this->getDbTable('resource')->select(['id' => $resourceOrId])->current() : $resourceOrId; - return $resource->addOrUpdateRating(is_int($userOrId) ? $userOrId : $userOrId->getId(), $rating); + if (null !== $rating && ($rating < 0 || $rating > 100)) { + throw new \Exception('Rating value out of range'); + } + + $dql = 'SELECT r ' + . 'FROM ' . RatingsEntityInterface::class . ' r ' + . 'WHERE r.user = :user AND r.resource = :resource'; + $resource = $this->getDoctrineReference(ResourceEntityInterface::class, $resourceOrId); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $parameters = compact('resource', 'user'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + + if ($existing = current($query->getResult())) { + if (null === $rating) { + $this->entityManager->remove($existing); + } else { + $existing->setRating($rating); + } + $updatedRatingId = $existing->getId(); + try { + $this->entityManager->flush(); + } catch (\Exception $e) { + $this->logError('Rating update failed: ' . $e->getMessage()); + throw $e; + } + return $updatedRatingId; + } + + if (null === $rating) { + return 0; + } + + $row = $this->createEntity() + ->setResource($resource) + ->setUser($user) + ->setRating($rating) + ->setCreated(new \DateTime()); + try { + $this->persistEntity($row); + } catch (\Exception $e) { + $this->logError('Could not save rating: ' . $e->getMessage()); + return 0; + } + return $row->getId(); + } + + /** + * Create a ratings entity. + * + * @return RatingsEntityInterface + */ + public function createEntity(): RatingsEntityInterface + { + return $this->entityPluginManager->get(RatingsEntityInterface::class); } /** @@ -137,11 +273,15 @@ public function addOrUpdateRating( */ public function deleteByIdsAndUserId(array $ids, int $userId): void { - $callback = function ($select) use ($ids, $userId) { - $select->where->in('id', $ids); - $select->where->equalTo('user_id', $userId); - }; - $this->getDbTable('Ratings')->delete($callback); + $dql = 'DELETE FROM ' . RatingsEntityInterface::class . ' ra ' + . 'WHERE ra.user = :user AND ra.id IN (:ids)'; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters([ + 'user' => $userId, + 'ids' => $ids, + ]); + $query->execute(); } /** @@ -152,19 +292,39 @@ public function deleteByIdsAndUserId(array $ids, int $userId): void * @param int $page Page * @param string $sort Sort * - * @return \Laminas\Paginator\Paginator + * @return Paginator */ public function getRatingsPaginator( int $userId, int $limit, int $page, - string $sort, - ): \Laminas\Paginator\Paginator { - return $this->getDbTable('Ratings')->getRatingsPaginator( - $userId, - $limit, - $page, - $sort, - ); + string $sort + ): Paginator { + $dql = 'SELECT r.id, r.rating, r.created AS created, ' + . 'u.id AS user_id, u.username AS username, ' + . 'res.id AS resource_id, res.recordId AS record_id, res.source AS source, res.title AS title ' + . 'FROM ' . RatingsEntityInterface::class . ' r ' + . 'LEFT JOIN r.user u ' + . 'LEFT JOIN r.resource res ' + . 'WHERE r.user = :userId'; + + $parameters = ['userId' => $userId]; + + $sortOrder = $sort ? $sort : 'created DESC'; + + $dql .= ' ORDER BY ' . $sortOrder; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->setFirstResult(($page - 1) * $limit) + ->setMaxResults($limit); + + $doctrinePaginator = new DoctrinePaginator($query); + $doctrinePaginator->setUseOutputWalkers(false); + + $paginator = new Paginator(new DoctrinePaginatorAdapter($doctrinePaginator)); + $paginator->setItemCountPerPage($limit); + $paginator->setCurrentPageNumber($page); + return $paginator; } } diff --git a/module/VuFind/src/VuFind/Db/Service/RecordService.php b/module/VuFind/src/VuFind/Db/Service/RecordService.php index 1eff27251d5..138f95c5dc0 100644 --- a/module/VuFind/src/VuFind/Db/Service/RecordService.php +++ b/module/VuFind/src/VuFind/Db/Service/RecordService.php @@ -31,9 +31,12 @@ namespace VuFind\Db\Service; use Exception; +use VuFind\Db\Entity\Record; use VuFind\Db\Entity\RecordEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\UserResourceEntityInterface; + +use function count; /** * Database service for Records. @@ -44,10 +47,8 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class RecordService extends AbstractDbService implements DbTableAwareInterface, RecordServiceInterface +class RecordService extends AbstractDbService implements RecordServiceInterface { - use DbTableAwareTrait; - /** * Retrieve a record by id. * @@ -58,7 +59,14 @@ class RecordService extends AbstractDbService implements DbTableAwareInterface, */ public function getRecord(string $id, string $source): ?RecordEntityInterface { - return $this->getDbTable('record')->findRecord($id, $source); + $dql = 'SELECT r ' + . 'FROM ' . RecordEntityInterface::class . ' r ' + . 'WHERE r.recordId = :id AND r.source = :source'; + $parameters = compact('id', 'source'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $records = $query->getResult(); + return count($records) > 0 ? current($records) : null; } /** @@ -71,7 +79,18 @@ public function getRecord(string $id, string $source): ?RecordEntityInterface */ public function getRecords(array $ids, string $source): array { - return $this->getDbTable('record')->findRecords($ids, $source); + if (empty($ids)) { + return []; + } + + $dql = 'SELECT r ' + . 'FROM ' . RecordEntityInterface::class . ' r ' + . 'WHERE r.recordId IN (:ids) AND r.source = :source'; + $parameters = compact('ids', 'source'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $records = $query->getResult(); + return $records; } /** @@ -105,12 +124,25 @@ public function updateRecord(string $id, string $source, $rawData): RecordEntity */ public function cleanup(): int { - return $this->getDbTable('record')->cleanup(); + $dql = 'SELECT r.id ' + . 'FROM ' . RecordEntityInterface::class . ' r ' + . 'JOIN ' . ResourceEntityInterface::class . ' re ' + . 'WITH r.recordId = re.recordId AND r.source = re.source ' + . 'LEFT JOIN ' . UserResourceEntityInterface::class . ' ur ' + . 'WITH re.id = ur.resource ' + . 'WHERE ur.id IS NULL'; + $query = $this->entityManager->createQuery($dql); + $ids = $query->getResult(); + $dql = 'DELETE FROM ' . RecordEntityInterface::class . ' r ' + . 'WHERE r.id IN (:ids)'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('ids')); + $query->execute(); + return count($ids); } /** - * Delete a record by source and id. Return true if found and deleted, false if not found. - * Throws exception if something goes wrong. + * Delete a record by source and id * * @param string $id Record ID * @param string $source Record source @@ -120,12 +152,13 @@ public function cleanup(): int */ public function deleteRecord(string $id, string $source): bool { - $record = $this->getDbTable('record')->findRecord($id, $source); - if (!$record) { - return false; - } - $record->delete(); - return true; + $dql = 'DELETE FROM ' . RecordEntityInterface::class . ' r ' + . 'WHERE r.recordId = :id AND r.source = :source'; + $parameters = compact('id', 'source'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->execute(); + return $result; } /** @@ -135,6 +168,6 @@ public function deleteRecord(string $id, string $source): bool */ public function createEntity(): RecordEntityInterface { - return $this->getDbTable('record')->createRow(); + return $this->entityPluginManager->get(RecordEntityInterface::class); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ResourceService.php b/module/VuFind/src/VuFind/Db/Service/ResourceService.php index 6317a454235..6be3210f27a 100644 --- a/module/VuFind/src/VuFind/Db/Service/ResourceService.php +++ b/module/VuFind/src/VuFind/Db/Service/ResourceService.php @@ -30,13 +30,17 @@ namespace VuFind\Db\Service; +use Doctrine\ORM\EntityManager; use Exception; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\PluginManager as EntityPluginManager; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\ResourceTagsEntityInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Table\Resource; - -use function count; +use VuFind\Db\Entity\UserResourceEntityInterface; +use VuFind\Db\PersistenceManager; +use VuFind\Log\LoggerAwareTrait; /** * Database service for resource. @@ -44,19 +48,42 @@ * @category VuFind * @package Database * @author Demian Katz - * @author Sudharma Kellampalli * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class ResourceService extends AbstractDbService implements ResourceServiceInterface, Feature\TransactionInterface +class ResourceService extends AbstractDbService implements + ResourceServiceInterface, + DbServiceAwareInterface, + Feature\TransactionInterface, + LoggerAwareInterface { + use DbServiceAwareTrait; + use Feature\ResourceSortTrait; + use LoggerAwareTrait; + /** - * Constructor. + * Callback to load the resource populator. * - * @param Resource $resourceTable Resource table + * @var callable */ - public function __construct(protected Resource $resourceTable) - { + protected $resourcePopulatorLoader; + + /** + * Constructor + * + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager VuFind entity plugin manager + * @param PersistenceManager $persistenceManager Entity persistence manager + * @param callable $resourcePopulatorLoader Resource populator + */ + public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, + PersistenceManager $persistenceManager, + callable $resourcePopulatorLoader + ) { + $this->resourcePopulatorLoader = $resourcePopulatorLoader; + parent::__construct($entityManager, $entityPluginManager, $persistenceManager); } /** @@ -67,7 +94,7 @@ public function __construct(protected Resource $resourceTable) */ public function beginTransaction(): void { - $this->resourceTable->beginTransaction(); + $this->entityManager->getConnection()->beginTransaction(); } /** @@ -78,7 +105,7 @@ public function beginTransaction(): void */ public function commitTransaction(): void { - $this->resourceTable->commitTransaction(); + $this->entityManager->getConnection()->commit(); } /** @@ -89,7 +116,7 @@ public function commitTransaction(): void */ public function rollBackTransaction(): void { - $this->resourceTable->rollbackTransaction(); + $this->entityManager->getConnection()->rollBack(); } /** @@ -101,7 +128,8 @@ public function rollBackTransaction(): void */ public function getResourceById(int $id): ?ResourceEntityInterface { - return $this->resourceTable->select(['id' => $id])->current(); + $resource = $this->entityManager->find(ResourceEntityInterface::class, $id); + return $resource; } /** @@ -111,7 +139,7 @@ public function getResourceById(int $id): ?ResourceEntityInterface */ public function createEntity(): ResourceEntityInterface { - return $this->resourceTable->createRow(); + return $this->entityPluginManager->get(ResourceEntityInterface::class); } /** @@ -122,12 +150,13 @@ public function createEntity(): ResourceEntityInterface */ public function findMissingMetadata(): array { - $callback = function ($select) { - $select->where->equalTo('title', '') - ->OR->isNull('author') - ->OR->isNull('year'); - }; - return iterator_to_array($this->resourceTable->select($callback)); + $dql = 'SELECT r ' + . 'FROM ' . ResourceEntityInterface::class . ' r ' + . "WHERE r.title = '' OR r.author IS NULL OR r.year IS NULL"; + + $query = $this->entityManager->createQuery($dql); + $result = $query->getResult(); + return $result; } /** @@ -140,7 +169,7 @@ public function findMissingMetadata(): array */ public function getResourceByRecordId(string $id, string $source = DEFAULT_SEARCH_BACKEND): ?ResourceEntityInterface { - return $this->resourceTable->select(['record_id' => $id, 'source' => $source])->current(); + return current($this->getResourcesByRecordIds([$id], $source)) ?: null; } /** @@ -153,11 +182,46 @@ public function getResourceByRecordId(string $id, string $source = DEFAULT_SEARC */ public function getResourcesByRecordIds(array $ids, string $source = DEFAULT_SEARCH_BACKEND): array { - $callback = function ($select) use ($ids, $source) { - $select->where->in('record_id', $ids); - $select->where->equalTo('source', $source); - }; - return iterator_to_array($this->resourceTable->select($callback)); + $repo = $this->entityManager->getRepository(ResourceEntityInterface::class); + $criteria = [ + 'recordId' => $ids, + 'source' => $source, + ]; + return $repo->findBy($criteria); + } + + /** + * Get resources associated with a particular tag. + * + * @param string $tag Tag to match + * @param int $user ID of user owning favorite list + * @param ?int $list ID of list to retrieve (null for all favorites) + * @param bool $caseSensitiveTags Should tags be treated case sensitively? + * + * @return array + */ + protected function getResourceIDsForTag( + string $tag, + int $user, + ?int $list = null, + bool $caseSensitiveTags = false + ): array { + $dql = 'SELECT DISTINCT(rt.resource) AS resource_id ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'JOIN rt.tag t ' + . 'WHERE ' . ($caseSensitiveTags ? 't.tag = :tag' : 'LOWER(t.tag) = LOWER(:tag) ') + . 'AND rt.user = :user'; + + $user = $this->getDoctrineReference(UserEntityInterface::class, $user); + $parameters = compact('tag', 'user'); + if (null !== $list) { + $list = $this->getDoctrineReference(UserListEntityInterface::class, $list); + $dql .= ' AND rt.list = :list'; + $parameters['list'] = $list; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getSingleColumnResult(); } /** @@ -183,17 +247,53 @@ public function getFavorites( ?int $limit = null, bool $caseSensitiveTags = false ): array { - return iterator_to_array( - $this->resourceTable->getFavorites( - $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId, - $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId, - $tags, - $sort, - $offset, - $limit, - $caseSensitiveTags - ) - ); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $list = $listOrId ? $this->getDoctrineReference(UserListEntityInterface::class, $listOrId) : null; + $orderByDetails = empty($sort) ? [] : $this->getResourceOrderByClause($sort); + $dql = 'SELECT DISTINCT r'; + if (!empty($orderByDetails['extraSelect'])) { + $dql .= ', ' . $orderByDetails['extraSelect']; + } + $dql .= ' FROM ' . ResourceEntityInterface::class . ' r ' + . 'JOIN ' . UserResourceEntityInterface::class . ' ur WITH r.id = ur.resource '; + $dqlWhere = []; + $dqlWhere[] = 'ur.user = :user'; + $parameters = compact('user'); + if (null !== $list) { + $dqlWhere[] = 'ur.list = :list'; + $parameters['list'] = $list; + } + + // Adjust for tags if necessary: + if (!empty($tags)) { + $matches = null; + foreach ($tags as $tag) { + $nextTagBatch = $this->getResourceIDsForTag($tag, $user->getId(), $list?->getId(), $caseSensitiveTags); + $matches = array_intersect( + $matches ?? $nextTagBatch, // first time, use whole batch + $nextTagBatch + ); + } + $dqlWhere[] = 'r.id IN (:ids)'; + $parameters['ids'] = $matches; + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + if (!empty($orderByDetails['orderByClause'])) { + $dql .= $orderByDetails['orderByClause']; + } + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + + if ($offset > 0) { + $query->setFirstResult($offset); + } + if (null !== $limit) { + $query->setMaxResults($limit); + } + + $result = $query->getResult(); + return $result; } /** @@ -208,12 +308,12 @@ public function getFavorites( */ public function deleteResourceByRecordId(string $id, string $source): bool { - $row = $this->resourceTable->select(['source' => $source, 'record_id' => $id])->current(); - if (!$row) { - return false; - } - $row->delete(); - return true; + $dql = 'DELETE FROM ' . ResourceEntityInterface::class . ' r ' + . 'WHERE r.recordId = :id AND r.source = :source'; + $parameters = compact('id', 'source'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->execute(); } /** @@ -226,12 +326,11 @@ public function deleteResourceByRecordId(string $id, string $source): bool */ public function renameSource(string $old, string $new): int { - $resourceWhere = ['source' => $old]; - $resourceRows = $this->resourceTable->select($resourceWhere); - if ($count = count($resourceRows)) { - $this->resourceTable->update(['source' => $new], $resourceWhere); - } - return $count; + $dql = 'UPDATE ' . ResourceEntityInterface::class . ' r ' + . 'SET r.source=:new WHERE r.source=:old'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('new', 'old')); + return $query->execute(); } /** @@ -243,7 +342,6 @@ public function renameSource(string $old, string $new): int */ public function deleteResource(ResourceEntityInterface|int $resourceOrId): void { - $id = $resourceOrId instanceof ResourceEntityInterface ? $resourceOrId->getId() : $resourceOrId; - $this->resourceTable->delete(['id' => $id]); + $this->deleteEntity($this->getDoctrineReference(ResourceEntityInterface::class, $resourceOrId)); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php b/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php index f4be49e4969..6272091e8ed 100644 --- a/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php +++ b/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php @@ -33,6 +33,7 @@ use Laminas\ServiceManager\Exception\ServiceNotFoundException; use Psr\Container\ContainerExceptionInterface as ContainerException; use Psr\Container\ContainerInterface; +use VuFind\Record\ResourcePopulator; /** * Database resource service factory @@ -67,7 +68,9 @@ public function __invoke( if (!empty($options)) { throw new \Exception('Unexpected options sent to factory!'); } - $table = $container->get(\VuFind\Db\Table\PluginManager::class)->get('resource'); - return parent::__invoke($container, $requestedName, [$table]); + $populatorLoader = function () use ($container) { + return $container->get(ResourcePopulator::class); + }; + return parent::__invoke($container, $requestedName, [$populatorLoader]); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php b/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php index 0983bcb119b..c82c5086091 100644 --- a/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php +++ b/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php @@ -30,6 +30,8 @@ namespace VuFind\Db\Service; use DateTime; +use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator; +use DoctrineORMModule\Paginator\Adapter\DoctrinePaginator as DoctrinePaginatorAdapter; use Laminas\Paginator\Paginator; use VuFind\Db\Entity\ResourceEntityInterface; use VuFind\Db\Entity\ResourceTagsEntityInterface; @@ -37,7 +39,8 @@ use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; -use function is_int; +use function count; +use function in_array; /** * Database service for resource_tags. @@ -49,11 +52,37 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class ResourceTagsService extends AbstractDbService implements + DbServiceAwareInterface, ResourceTagsServiceInterface, - Feature\TransactionInterface, - \VuFind\Db\Table\DbTableAwareInterface + Feature\TransactionInterface { - use \VuFind\Db\Table\DbTableAwareTrait; + use DbServiceAwareTrait; + + /** + * Given an array for sorting database results, make sure the tag field is + * sorted in a case-insensitive fashion and that no illegal fields are + * specified. + * + * @param array $order Order settings + * + * @return array + */ + protected function formatTagOrder(array $order) + { + // This array defines legal sort fields: + $legalSorts = ['tag', 'title', 'username', 'posted asc', 'posted desc']; + $newOrder = []; + foreach ($order as $next) { + if (in_array($next, $legalSorts)) { + if ('posted asc' === $next || 'posted desc' === $next) { + $newOrder[] = $next; + } else { + $newOrder[] = $next . 'Sort ASC'; + } + } + } + return $newOrder; + } /** * Begin a database transaction. @@ -63,7 +92,7 @@ class ResourceTagsService extends AbstractDbService implements */ public function beginTransaction(): void { - $this->getDbTable('ResourceTags')->beginTransaction(); + $this->entityManager->getConnection()->beginTransaction(); } /** @@ -74,7 +103,7 @@ public function beginTransaction(): void */ public function commitTransaction(): void { - $this->getDbTable('ResourceTags')->commitTransaction(); + $this->entityManager->getConnection()->commit(); } /** @@ -85,7 +114,7 @@ public function commitTransaction(): void */ public function rollBackTransaction(): void { - $this->getDbTable('ResourceTags')->rollbackTransaction(); + $this->entityManager->getConnection()->rollBack(); } /** @@ -110,8 +139,51 @@ public function getResourceTagsPaginator( int $limit = 20, bool $caseSensitiveTags = false ): Paginator { - return $this->getDbTable('ResourceTags') - ->getResourceTags($userId, $resourceId, $tagId, $order, $page, $limit, $caseSensitiveTags); + $tag = $caseSensitiveTags ? 't.tag' : 'lower(t.tag)'; + $dql = 'SELECT rt.id, ' . $tag . ' AS tag, rt.posted AS posted, u.username AS username, r.title AS title,' + . ' t.id AS tag_id, r.id AS resource_id, r.source AS source, r.recordId AS record_id, u.id AS user_id,' + . ' lower(t.tag) AS HIDDEN tagSort, lower(u.username) AS HIDDEN usernameSort,' + . ' lower(r.title) AS HIDDEN titleSort ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'LEFT JOIN rt.resource r ' + . 'LEFT JOIN rt.tag t ' + . 'LEFT JOIN rt.user u'; + $parameters = $dqlWhere = []; + if (null !== $userId) { + $dqlWhere[] = 'rt.user = :user'; + $parameters['user'] = $userId; + } + if (null !== $resourceId) { + $dqlWhere[] = 'r.id = :resource'; + $parameters['resource'] = $resourceId; + } + if (null !== $tagId) { + $dqlWhere[] = 'rt.tag = :tag'; + $parameters['tag'] = $tagId; + } + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $sanitizedOrder = $this->formatTagOrder( + (array)($order ?? ['username', 'tag', 'title']) + ); + $dql .= ' ORDER BY ' . implode(', ', $sanitizedOrder); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + + if (null !== $page) { + $query->setMaxResults($limit); + $query->setFirstResult($limit * ($page - 1)); + } + + $doctrinePaginator = new DoctrinePaginator($query); + $doctrinePaginator->setUseOutputWalkers(false); + $paginator = new Paginator(new DoctrinePaginatorAdapter($doctrinePaginator)); + if (null !== $page) { + $paginator->setCurrentPageNumber($page); + $paginator->setItemCountPerPage($limit); + } + return $paginator; } /** @@ -121,7 +193,7 @@ public function getResourceTagsPaginator( */ public function createEntity(): ResourceTagsEntityInterface { - return $this->getDbTable('ResourceTags')->createRow(); + return $this->entityPluginManager->get(ResourceTagsEntityInterface::class); } /** @@ -142,41 +214,56 @@ public function createLink( UserListEntityInterface|int|null $listOrId = null, ?DateTime $posted = null ) { - $table = $this->getDbTable('ResourceTags'); - $resourceId = is_int($resourceOrId) ? $resourceOrId : $resourceOrId?->getId(); - $tagId = is_int($tagOrId) ? $tagOrId : $tagOrId->getId(); - $userId = is_int($userOrId) ? $userOrId : $userOrId?->getId(); - $listId = is_int($listOrId) ? $listOrId : $listOrId?->getId(); - - $callback = function ($select) use ($resourceId, $tagId, $userId, $listId) { - $select->where->equalTo('resource_id', $resourceId) - ->equalTo('tag_id', $tagId); - if (null !== $listId) { - $select->where->equalTo('list_id', $listId); - } else { - $select->where->isNull('list_id'); - } - if (null !== $userId) { - $select->where->equalTo('user_id', $userId); - } else { - $select->where->isNull('user_id'); - } - }; - $result = $table->select($callback)->current(); + $tag = $this->getDoctrineReference(TagsEntityInterface::class, $tagOrId); + $dql = ' SELECT rt FROM ' . ResourceTagsEntityInterface::class . ' rt '; + $dqlWhere = ['rt.tag = :tag ']; + $parameters = compact('tag'); + + if (null !== $resourceOrId) { + $resource = $this->getDoctrineReference(ResourceEntityInterface::class, $resourceOrId); + $dqlWhere[] = 'rt.resource = :resource '; + $parameters['resource'] = $resource; + } else { + $resource = null; + $dqlWhere[] = 'rt.resource IS NULL '; + } + + if (null !== $listOrId) { + $list = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + $dqlWhere[] = 'rt.list = :list '; + $parameters['list'] = $list; + } else { + $list = null; + $dqlWhere[] = 'rt.list IS NULL '; + } + + if (null !== $userOrId) { + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $dqlWhere[] = 'rt.user = :user'; + $parameters['user'] = $user; + } else { + $user = null; + $dqlWhere[] = 'rt.user IS NULL '; + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = current($query->getResult()); // Only create row if it does not already exist: - if (!$result) { - $result = $this->createEntity(); - $result->resource_id = $resourceId; - $result->tag_id = $tagId; - if (null !== $listId) { - $result->list_id = $listId; + if (empty($result)) { + $row = $this->createEntity() + ->setResource($resource) + ->setTag($tag); + if (null !== $list) { + $row->setUserList($list); } - if (null !== $userId) { - $result->user_id = $userId; + if (null !== $user) { + $row->setUser($user); } - $result->setPosted($posted ?? new DateTime()); - $this->persistEntity($result); + $row->setPosted($posted ?? new DateTime()); + $this->persistEntity($row); } } @@ -189,7 +276,50 @@ public function createLink( */ public function deleteLinksByResourceTagsIdArray(array $ids): int { - return $this->getDbTable('ResourceTags')->deleteByIdArray($ids); + $dql = 'DELETE FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'WHERE rt.id IN (:ids)'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(compact('ids')); + $query->execute(); + return count($ids); + } + + /** + * Support method for the other destroyResourceTagsLinksForUser methods. + * + * @param int|int[]|null $resourceId ID (or array of IDs) of resource(s) to + * unlink (null for ALL matching resources) + * @param UserEntityInterface|int $userOrId ID or entity representing user + * @param int|int[]|null $tagId ID or array of IDs of tag(s) to unlink (null + * for ALL matching tags) + * @param array $extraWhere Extra where clauses for query + * @param array $extraParams Extra parameters for query + * + * @return void + */ + protected function destroyResourceTagsLinksForUserWithDoctrine( + int|array|null $resourceId, + UserEntityInterface|int $userOrId, + int|array|null $tagId = null, + $extraWhere = [], + $extraParams = [], + ) { + $dql = 'DELETE FROM ' . ResourceTagsEntityInterface::class . ' rt '; + + $dqlWhere = ['rt.user = :user ']; + $parameters = ['user' => $this->getDoctrineReference(UserEntityInterface::class, $userOrId)]; + if (null !== $resourceId) { + $dqlWhere[] = 'rt.resource IN (:resource) '; + $parameters['resource'] = (array)$resourceId; + } + if (null !== $tagId) { + $dqlWhere[] = 'rt.tag IN (:tag) '; + $parameters['tag'] = (array)$tagId; + } + $dql .= ' WHERE ' . implode(' AND ', array_merge($dqlWhere, $extraWhere)); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters + $extraParams); + $query->execute(); } /** @@ -210,21 +340,13 @@ public function destroyResourceTagsLinksForUser( UserListEntityInterface|int|null $listOrId = null, int|array|null $tagId = null ): void { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $callback = function ($select) use ($resourceId, $userId, $listId, $tagId) { - $select->where->equalTo('user_id', $userId); - if (null !== $resourceId) { - $select->where->in('resource_id', (array)$resourceId); - } - if (null !== $listId) { - $select->where->equalTo('list_id', $listId); - } - if (null !== $tagId) { - $select->where->in('tag_id', (array)$tagId); - } - }; - $this->getDbTable('ResourceTags')->delete($callback); + $dqlWhere = $parameters = []; + if (null !== $listOrId) { + $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; + $dqlWhere[] = 'rt.list = :list'; + $parameters['list'] = $listId; + } + $this->destroyResourceTagsLinksForUserWithDoctrine($resourceId, $userOrId, $tagId, $dqlWhere, $parameters); } /** @@ -242,18 +364,8 @@ public function destroyNonListResourceTagsLinksForUser( UserEntityInterface|int $userOrId, int|array|null $tagId = null ): void { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($resourceId, $userId, $tagId) { - $select->where->equalTo('user_id', $userId); - if (null !== $resourceId) { - $select->where->in('resource_id', (array)$resourceId); - } - $select->where->isNull('list_id'); - if (null !== $tagId) { - $select->where->in('tag_id', (array)$tagId); - } - }; - $this->getDbTable('ResourceTags')->delete($callback); + $dqlWhere = ['rt.list IS NULL ']; + $this->destroyResourceTagsLinksForUserWithDoctrine($resourceId, $userOrId, $tagId, $dqlWhere); } /** @@ -272,18 +384,8 @@ public function destroyAllListResourceTagsLinksForUser( UserEntityInterface|int $userOrId, int|array|null $tagId = null ): void { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($resourceId, $userId, $tagId) { - $select->where->equalTo('user_id', $userId); - if (null !== $resourceId) { - $select->where->in('resource_id', (array)$resourceId); - } - $select->where->isNotNull('list_id'); - if (null !== $tagId) { - $select->where->in('tag_id', (array)$tagId); - } - }; - $this->getDbTable('ResourceTags')->delete($callback); + $dqlWhere = ['rt.list IS NOT NULL ']; + $this->destroyResourceTagsLinksForUserWithDoctrine($resourceId, $userOrId, $tagId, $dqlWhere); } /** @@ -301,20 +403,18 @@ public function destroyUserListLinks( UserEntityInterface|int $userOrId, int|array|null $tagId = null ): void { - $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($userId, $listId, $tagId) { - $select->where->equalTo('user_id', $userId); - // retrieve tags assigned to a user list and filter out user resource tags - // (resource_id is NULL for list tags). - $select->where->isNull('resource_id'); - $select->where->equalTo('list_id', $listId); - - if (null !== $tagId) { - $select->where->in('tag_id', (array)$tagId); - } - }; - $this->getDbTable('ResourceTags')->delete($callback); + $list = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $dql = 'DELETE FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'WHERE rt.user = :user AND rt.resource IS NULL AND rt.list = :list '; + $parameters = compact('user', 'list'); + if (null !== $tagId) { + $dqlWhere[] = 'AND rt.tag IN (:tag) '; + $parameters['tag'] = (array)$tagId; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -331,7 +431,32 @@ public function getUniqueResources( ?int $resourceId = null, ?int $tagId = null ): array { - return $this->getDbTable('ResourceTags')->getUniqueResources($userId, $resourceId, $tagId)->toArray(); + $dql = 'SELECT r.id AS resource_id, MAX(rt.tag) AS tag_id, ' + . 'MAX(rt.list) AS list_id, MAX(rt.user) AS user_id, MAX(rt.id) AS id, ' + . 'r.title AS title ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'LEFT JOIN rt.resource r '; + $parameters = $dqlWhere = []; + if (null !== $userId) { + $dqlWhere[] = 'rt.user = :user'; + $parameters['user'] = $userId; + } + if (null !== $resourceId) { + $dqlWhere[] = 'r.id = :resource'; + $parameters['resource'] = $resourceId; + } + if (null !== $tagId) { + $dqlWhere[] = 'rt.tag = :tag'; + $parameters['tag'] = $tagId; + } + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $dql .= ' GROUP BY resource_id, title' + . ' ORDER BY title'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getResult(); } /** @@ -350,8 +475,42 @@ public function getUniqueTags( ?int $tagId = null, bool $caseSensitive = false ): array { - return $this->getDbTable('ResourceTags')->getUniqueTags($userId, $resourceId, $tagId, $caseSensitive) - ->toArray(); + if ($caseSensitive) { + $tagClause = 't.tag AS tag'; + $sort = 'LOWER(t.tag), tag'; + } else { + $tagClause = 'LOWER(t.tag) AS tag, MAX(t.tag) AS HIDDEN tagTiebreaker'; + $sort = 'tag, tagTiebreaker'; + } + $dql = 'SELECT MAX(r.id) AS resource_id, MAX(t.id) AS tag_id, ' + . 'MAX(l.id) AS list_id, MAX(u.id) AS user_id, MAX(rt.id) AS id, ' + . $tagClause + . ' FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'LEFT JOIN rt.resource r ' + . 'LEFT JOIN rt.tag t ' + . 'LEFT JOIN rt.list l ' + . 'LEFT JOIN rt.user u'; + $parameters = $dqlWhere = []; + if (null !== $userId) { + $dqlWhere[] = 'u.id = :user'; + $parameters['user'] = $userId; + } + if (null !== $resourceId) { + $dqlWhere[] = 'r.id = :resource'; + $parameters['resource'] = $resourceId; + } + if (null !== $tagId) { + $dqlWhere[] = 't.id = :tag'; + $parameters['tag'] = $tagId; + } + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $dql .= ' GROUP BY tag' + . ' ORDER BY ' . $sort; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getResult(); } /** @@ -368,7 +527,32 @@ public function getUniqueUsers( ?int $resourceId = null, ?int $tagId = null ): array { - return $this->getDbTable('ResourceTags')->getUniqueUsers($userId, $resourceId, $tagId)->toArray(); + $dql = 'SELECT MAX(rt.resource) AS resource_id, MAX(rt.tag) AS tag_id, ' + . 'MAX(rt.list) AS list_id, u.id AS user_id, MAX(rt.id) AS id, ' + . 'u.username AS username ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'INNER JOIN rt.user u '; + $parameters = $dqlWhere = []; + if (null !== $userId) { + $dqlWhere[] = 'rt.user = :user'; + $parameters['user'] = $userId; + } + if (null !== $resourceId) { + $dqlWhere[] = 'rt.resource = :resource'; + $parameters['resource'] = $resourceId; + } + if (null !== $tagId) { + $dqlWhere[] = 'rt.tag = :tag'; + $parameters['tag'] = $tagId; + } + if (!empty($dqlWhere)) { + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + } + $dql .= ' GROUP BY user_id, username' + . ' ORDER BY username'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getResult(); } /** @@ -406,7 +590,12 @@ public function deleteResourceTags( */ public function getAnonymousCount(): int { - return $this->getDbTable('ResourceTags')->getAnonymousCount(); + $dql = 'SELECT COUNT(rt.id) AS total ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'WHERE rt.user IS NULL'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats['total']; } /** @@ -418,8 +607,13 @@ public function getAnonymousCount(): int */ public function assignAnonymousTags(UserEntityInterface|int $userOrId): void { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $this->getDbTable('ResourceTags')->assignAnonymousTags($userId); + $id = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; + $dql = 'UPDATE ' . ResourceTagsEntityInterface::class . ' rt ' + . 'SET rt.user = :id WHERE rt.user is NULL'; + $parameters = compact('id'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -432,7 +626,30 @@ public function assignAnonymousTags(UserEntityInterface|int $userOrId): void */ public function changeResourceId(int $old, int $new): void { - $this->getDbTable('ResourceTags')->update(['resource_id' => $new], ['resource_id' => $old]); + $dql = 'UPDATE ' . ResourceTagsEntityInterface::class . ' e ' + . 'SET e.resource = :new WHERE e.resource = :old'; + $parameters = compact('new', 'old'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); + } + + /** + * Get a list of duplicate resource_tags rows (this sometimes happens after merging IDs, + * for example after a Summon resource ID changes). + * + * @return array + */ + protected function getDuplicateResourceLinks(): array + { + $dql = 'SELECT MIN(rt.resource) as resource_id, MiN(rt.tag) as tag_id, MIN(rt.list) as list_id, ' + . 'MIN(rt.user) as user_id, COUNT(rt.resource) as cnt, MIN(rt.id) as id ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'GROUP BY rt.resource, rt.tag, rt.list, rt.user ' + . 'HAVING COUNT(rt.resource) > 1'; + $query = $this->entityManager->createQuery($dql); + $result = $query->getResult(); + return $result; } /** @@ -442,6 +659,29 @@ public function changeResourceId(int $old, int $new): void */ public function deduplicate(): void { - $this->getDbTable('ResourceTags')->deduplicate(); + // match on all relevant IDs in duplicate group + // getDuplicates returns the minimum id in the set, so we want to + // delete all of the duplicates with a higher id value. + foreach ($this->getDuplicateResourceLinks() as $dupe) { + $dql = 'DELETE FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'WHERE rt.resource = :resource AND rt.tag = :tag ' + . 'AND rt.user = :user AND rt.id > :id'; + $parameters = [ + 'resource' => $dupe['resource_id'], + 'user' => $dupe['user_id'], + 'tag' => $dupe['tag_id'], + 'id' => $dupe['id'], + ]; + // List ID might be null (for record-level tags); this requires special handling. + if ($dupe['list_id'] !== null) { + $parameters['list'] = $dupe['list_id']; + $dql .= ' AND rt.list = :list '; + } else { + $dql .= ' AND rt.list IS NULL'; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); + } } } diff --git a/module/VuFind/src/VuFind/Db/Service/SearchService.php b/module/VuFind/src/VuFind/Db/Service/SearchService.php index 85e754bd080..2fcb04bc0c7 100644 --- a/module/VuFind/src/VuFind/Db/Service/SearchService.php +++ b/module/VuFind/src/VuFind/Db/Service/SearchService.php @@ -33,8 +33,6 @@ use Exception; use VuFind\Db\Entity\SearchEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for search. @@ -47,11 +45,8 @@ */ class SearchService extends AbstractDbService implements SearchServiceInterface, - Feature\DeleteExpiredInterface, - DbTableAwareInterface + Feature\DeleteExpiredInterface { - use DbTableAwareTrait; - /** * Create a search entity. * @@ -59,7 +54,7 @@ class SearchService extends AbstractDbService implements */ public function createEntity(): SearchEntityInterface { - return $this->getDbTable('search')->createRow(); + return $this->entityPluginManager->get(SearchEntityInterface::class); } /** @@ -74,18 +69,21 @@ public function createEntity(): SearchEntityInterface */ public function createAndPersistEntityWithChecksum(int $checksum): SearchEntityInterface { - $table = $this->getDbTable('search'); - $table->insert( - [ - 'created' => date('Y-m-d H:i:s'), - 'checksum' => $checksum, - ] - ); - $lastInsert = $table->getLastInsertValue(); - if (!($row = $this->getSearchById($lastInsert))) { - throw new Exception('Cannot find id ' . $lastInsert); + $entity = $this->createEntity(); + $entity->setCreated(new \DateTime()); + $entity->setChecksum($checksum); + + $this->persistEntity($entity); + $this->entityManager->flush(); + + $id = $entity->getId(); + $retrieved = $this->getSearchById($id); + + if (!$retrieved) { + throw new \Exception('Cannot find id ' . $id); } - return $row; + + return $retrieved; } /** @@ -98,15 +96,18 @@ public function createAndPersistEntityWithChecksum(int $checksum): SearchEntityI */ public function destroySession(string $sessionId, UserEntityInterface|int|null $userOrId = null): void { - $uid = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($sessionId, $uid) { - $select->where->equalTo('session_id', $sessionId)->and->equalTo('saved', 0); - if ($uid !== null) { - $select->where->OR - ->equalTo('user_id', $uid)->and->equalTo('saved', 0); - } - }; - $this->getDbTable('search')->delete($callback); + $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; + $parameters = ['saved' => false, 'sessionId' => $sessionId]; + $dql = 'DELETE FROM ' . SearchEntityInterface::class . ' s ' + . 'WHERE s.saved = :saved AND (s.sessionId = :sessionId'; + if ($userId !== null) { + $dql .= ' OR s.user = :userId'; + $parameters['userId'] = $userId; + } + $dql .= ')'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -118,7 +119,7 @@ public function destroySession(string $sessionId, UserEntityInterface|int|null $ */ public function getSearchById(int $id): ?SearchEntityInterface { - return $this->getDbTable('search')->select(['id' => $id])->current(); + return $this->entityManager->find(SearchEntityInterface::class, $id); } /** @@ -136,17 +137,25 @@ public function getSearchByIdAndOwner( UserEntityInterface|int|null $userOrId ): ?SearchEntityInterface { $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($id, $sessionId, $userId) { - $nest = $select->where - ->equalTo('id', $id) - ->and - ->nest - ->equalTo('session_id', $sessionId); - if (!empty($userId)) { - $nest->or->equalTo('user_id', $userId); - } - }; - return $this->getDbTable('search')->select($callback)->current(); + $entityClass = SearchEntityInterface::class; + + $dql = 'SELECT s FROM ' . $entityClass . ' s WHERE s.id = :id AND (s.sessionId = :sessionId'; + $parameters = [ + 'id' => $id, + 'sessionId' => $sessionId, + ]; + + if ($userId !== null) { + $dql .= ' OR s.user = :userId'; + $parameters['userId'] = $userId; + } + + $dql .= ')'; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + + return $query->getOneOrNullResult(); } /** @@ -159,22 +168,38 @@ public function getSearchByIdAndOwner( */ public function getSearches(?string $sessionId, UserEntityInterface|int|null $userOrId = null): array { - // If we don't get a session id or user id, don't return anything: - if (null === $sessionId && null === $userOrId) { + $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; + + if ($sessionId === null && $userId === null) { return []; } - $uid = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($sessionId, $uid) { - if (null !== $sessionId) { - $select->where->equalTo('session_id', $sessionId)->and->equalTo('saved', 0); - } - if ($uid !== null) { - // Note: It doesn't hurt to use OR here even if there are no other terms - $select->where->OR->equalTo('user_id', $uid); - } - $select->order('created'); - }; - return iterator_to_array($this->getDbTable('search')->select($callback)); + + $entityClass = SearchEntityInterface::class; + $dql = 'SELECT s FROM ' . $entityClass . ' s'; + $conditions = []; + $params = []; + + if ($sessionId !== null) { + $conditions[] = '(s.sessionId = :sessionId AND s.saved = :saved)'; + $params['sessionId'] = $sessionId; + $params['saved'] = false; + } + + if ($userId !== null) { + $conditions[] = 's.user = :userId'; + $params['userId'] = $userId; + } + + if ($conditions) { + $dql .= ' WHERE ' . implode(' OR ', $conditions); + } + + $dql .= ' ORDER BY s.created ASC'; + + return $this->entityManager + ->createQuery($dql) + ->setParameters($params) + ->getResult(); } /** @@ -184,12 +209,15 @@ public function getSearches(?string $sessionId, UserEntityInterface|int|null $us */ public function getScheduledSearches(): array { - $callback = function ($select) { - $select->where->equalTo('saved', 1); - $select->where->greaterThan('notification_frequency', 0); - $select->order('user_id'); - }; - return iterator_to_array($this->getDbTable('search')->select($callback)); + $entityClass = SearchEntityInterface::class; + $dql = 'SELECT s FROM ' . $entityClass + . ' s WHERE s.saved = :saved' + . ' AND s.notificationFrequency > 0' + . ' ORDER BY s.user ASC'; + + $query = $this->entityManager->createQuery($dql); + $query->setParameter('saved', true); + return $query->getResult(); } /** @@ -209,17 +237,19 @@ public function getSearchesByChecksumAndOwner( UserEntityInterface|int|null $userOrId = null ): array { $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($checksum, $sessionId, $userId) { - $nest = $select->where - ->equalTo('checksum', $checksum) - ->and - ->nest - ->equalTo('session_id', $sessionId)->and->equalTo('saved', 0); - if (!empty($userId)) { - $nest->or->equalTo('user_id', $userId); - } - }; - return iterator_to_array($this->getDbTable('search')->select($callback)); + $dql = 'SELECT s FROM ' . SearchEntityInterface::class . ' s ' + . 'WHERE s.checksum = :checksum AND '; + $extraClauses = ['(s.sessionId = :sessionId AND s.saved = :saved)']; + $params = ['checksum' => $checksum, 'saved' => false, 'sessionId' => $sessionId]; + if ($userId !== null) { + $extraClauses[] = 's.user = :userId'; + $params['userId'] = $userId; + } + $dql .= '(' . implode(' OR ', $extraClauses) . ')'; + return $this->entityManager + ->createQuery($dql) + ->setParameters($params) + ->getResult(); } /** @@ -229,8 +259,12 @@ public function getSearchesByChecksumAndOwner( */ public function getSavedSearchesWithMissingChecksums(): array { - $searchWhere = ['checksum' => null, 'saved' => 1]; - return iterator_to_array($this->getDbTable('search')->select($searchWhere)); + $dql = 'SELECT s FROM ' . SearchEntityInterface::class . ' s ' + . 'WHERE s.checksum IS NULL AND s.saved = :saved'; + + $query = $this->entityManager->createQuery($dql); + $query->setParameter('saved', true); + return $query->getResult(); } /** @@ -243,7 +277,11 @@ public function getSavedSearchesWithMissingChecksums(): array public function deleteSearch(SearchEntityInterface|int $searchOrId): void { $searchId = $searchOrId instanceof SearchEntityInterface ? $searchOrId->getId() : $searchOrId; - $this->getDbTable('search')->delete(['id' => $searchId]); + $dql = 'DELETE FROM ' . SearchEntityInterface::class . ' s' + . ' WHERE s.id = :searchId'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('searchId', $searchId); + $query->execute(); } /** @@ -256,6 +294,21 @@ public function deleteSearch(SearchEntityInterface|int $searchOrId): void */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->getDbTable('search')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('s.id') + ->from(SearchEntityInterface::class, 's') + ->where('s.created < :dateLimit AND s.saved = :saved') + ->setParameter('dateLimit', $dateLimit) + ->setParameter('saved', false); + + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(SearchEntityInterface::class, 's') + ->where('s.id IN (:searches)') + ->setParameter('searches', $subQueryBuilder->getQuery()->getResult()); + + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/SessionService.php b/module/VuFind/src/VuFind/Db/Service/SessionService.php index 1e86a5f9210..a63c2d240b8 100644 --- a/module/VuFind/src/VuFind/Db/Service/SessionService.php +++ b/module/VuFind/src/VuFind/Db/Service/SessionService.php @@ -31,9 +31,12 @@ namespace VuFind\Db\Service; use DateTime; +use Laminas\Log\LoggerAwareInterface; use VuFind\Db\Entity\SessionEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Exception\SessionExpired as SessionExpiredException; +use VuFind\Log\LoggerAwareTrait; + +use function intval; /** * Database service for Session. @@ -46,11 +49,11 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class SessionService extends AbstractDbService implements - DbTableAwareInterface, + LoggerAwareInterface, SessionServiceInterface, Feature\DeleteExpiredInterface { - use DbTableAwareTrait; + use LoggerAwareTrait; /** * Retrieve an object from the database based on session ID; create a new @@ -63,7 +66,26 @@ class SessionService extends AbstractDbService implements */ public function getSessionById(string $sid, bool $create = true): ?SessionEntityInterface { - return $this->getDbTable('Session')->getBySessionId($sid, $create); + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->select('s') + ->from(SessionEntityInterface::class, 's') + ->where('s.sessionId = :sid') + ->setParameter('sid', $sid); + $query = $queryBuilder->getQuery(); + $session = current($query->getResult()) ?: null; + if ($create && empty($session)) { + $now = new \DateTime(); + $session = $this->createEntity() + ->setSessionId($sid) + ->setCreated($now); + try { + $this->persistEntity($session); + } catch (\Exception $e) { + $this->logError('Could not save session: ' . $e->getMessage()); + return null; + } + } + return $session; } /** @@ -77,7 +99,27 @@ public function getSessionById(string $sid, bool $create = true): ?SessionEntity */ public function readSession(string $sid, int $lifetime): string { - return $this->getDbTable('Session')->readSession($sid, $lifetime); + $s = $this->getSessionById($sid); + if (!$s) { + throw new SessionExpiredException("Cannot read session $sid"); + } + $lastused = $s->getLastUsed(); + // enforce lifetime of this session data + if (!empty($lastused) && $lastused + $lifetime <= time()) { + throw new SessionExpiredException('Session expired!'); + } + + // if we got this far, session is good -- update last access time, save + // changes, and return data. + $s->setLastUsed(time()); + try { + $this->persistEntity($s); + } catch (\Exception $e) { + $this->logError('Could not save session: ' . $e->getMessage()); + return ''; + } + $data = $s->getData(); + return $data ?? ''; } /** @@ -90,7 +132,18 @@ public function readSession(string $sid, int $lifetime): string */ public function writeSession(string $sid, string $data): bool { - $this->getDbTable('Session')->writeSession($sid, $data); + $session = $this->getSessionById($sid); + try { + if (!$session) { + throw new \Exception("cannot read id $sid"); + } + $session->setLastUsed(time()) + ->setData($data); + $this->persistEntity($session); + } catch (\Exception $e) { + $this->logError('Could not save session data: ' . $e->getMessage()); + return false; + } return true; } @@ -103,7 +156,12 @@ public function writeSession(string $sid, string $data): bool */ public function destroySession(string $sid): void { - $this->getDbTable('Session')->destroySession($sid); + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(SessionEntityInterface::class, 's') + ->where('s.sessionId = :sid') + ->setParameter('sid', $sid); + $query = $queryBuilder->getQuery(); + $query->execute(); } /** @@ -115,7 +173,25 @@ public function destroySession(string $sid): void */ public function garbageCollect(int $maxLifetime): int { - return $this->getDbTable('Session')->garbageCollect($maxLifetime); + $expiration = time() - intval($maxLifetime); + + $entityClass = SessionEntityInterface::class; + + $dql = 'SELECT COUNT(s) FROM ' . $entityClass . ' s WHERE s.lastUsed < :used'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('used', $expiration); + $count = (int)$query->getSingleScalarResult(); + + if ($count > 0) { + $deleteQueryBuilder = $this->entityManager->createQueryBuilder(); + $deleteQueryBuilder->delete($entityClass, 's') + ->where('s.lastUsed < :used') + ->setParameter('used', $expiration); + $deleteQuery = $deleteQueryBuilder->getQuery(); + $deleteQuery->execute(); + } + + return $count; } /** @@ -125,7 +201,7 @@ public function garbageCollect(int $maxLifetime): int */ public function createEntity(): SessionEntityInterface { - return $this->getDbTable('Session')->createRow(); + return $this->entityPluginManager->get(SessionEntityInterface::class); } /** @@ -138,6 +214,18 @@ public function createEntity(): SessionEntityInterface */ public function deleteExpired(DateTime $dateLimit, ?int $limit = null): int { - return $this->getDbTable('Session')->deleteExpired($dateLimit->format('Y-m-d H:i:s'), $limit); + $subQueryBuilder = $this->entityManager->createQueryBuilder(); + $subQueryBuilder->select('s.id') + ->from(SessionEntityInterface::class, 's') + ->where('s.lastUsed < :used') + ->setParameter('used', $dateLimit->getTimestamp()); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete(SessionEntityInterface::class, 's') + ->where('s.id IN (:ids)') + ->setParameter('ids', $subQueryBuilder->getQuery()->getResult()); + return $queryBuilder->getQuery()->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php b/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php index c3a58b9ccda..4173d9b7ad2 100644 --- a/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php +++ b/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php @@ -30,10 +30,9 @@ namespace VuFind\Db\Service; +use DateTime; use Exception; use VuFind\Db\Entity\ShortlinksEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for shortlinks. @@ -46,12 +45,9 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class ShortlinksService extends AbstractDbService implements - DbTableAwareInterface, ShortlinksServiceInterface, Feature\TransactionInterface { - use DbTableAwareTrait; - /** * Begin a database transaction. * @@ -60,7 +56,7 @@ class ShortlinksService extends AbstractDbService implements */ public function beginTransaction(): void { - $this->getDbTable('shortlinks')->beginTransaction(); + $this->entityManager->getConnection()->beginTransaction(); } /** @@ -71,7 +67,7 @@ public function beginTransaction(): void */ public function commitTransaction(): void { - $this->getDbTable('shortlinks')->commitTransaction(); + $this->entityManager->getConnection()->commit(); } /** @@ -82,7 +78,7 @@ public function commitTransaction(): void */ public function rollBackTransaction(): void { - $this->getDbTable('shortlinks')->rollbackTransaction(); + $this->entityManager->getConnection()->rollBack(); } /** @@ -92,7 +88,7 @@ public function rollBackTransaction(): void */ public function createEntity(): ShortlinksEntityInterface { - return $this->getDbTable('shortlinks')->createRow(); + return $this->entityPluginManager->get(ShortlinksEntityInterface::class); } /** @@ -104,10 +100,11 @@ public function createEntity(): ShortlinksEntityInterface */ public function createAndPersistEntityForPath(string $path): ShortlinksEntityInterface { - $table = $this->getDbTable('shortlinks'); - $table->insert(['path' => $path]); - $id = $table->getLastInsertValue(); - return $table->select(['id' => $id])->current(); + $shortlink = $this->createEntity() + ->setPath($path) + ->setCreated(new DateTime()); + $this->persistEntity($shortlink); + return $shortlink; } /** @@ -119,7 +116,13 @@ public function createAndPersistEntityForPath(string $path): ShortlinksEntityInt */ public function getShortLinkByHash(string $hash): ?ShortlinksEntityInterface { - return $this->getDbTable('shortlinks')->select(['hash' => $hash])->current(); + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->select('s') + ->from(ShortlinksEntityInterface::class, 's') + ->where('s.hash = :hash') + ->setParameter('hash', $hash); + $query = $queryBuilder->getQuery(); + return $query->getResult()[0] ?? null; } /** @@ -129,6 +132,8 @@ public function getShortLinkByHash(string $hash): ?ShortlinksEntityInterface */ public function getShortLinksWithMissingHashes(): array { - return iterator_to_array($this->getDbTable('shortlinks')->select(['hash' => null])); + return $this->entityManager + ->getRepository(ShortlinksEntityInterface::class) + ->findBy(['hash' => null]); } } diff --git a/module/VuFind/src/VuFind/Db/Service/TagService.php b/module/VuFind/src/VuFind/Db/Service/TagService.php index 07cc31aa3f9..a916a958118 100644 --- a/module/VuFind/src/VuFind/Db/Service/TagService.php +++ b/module/VuFind/src/VuFind/Db/Service/TagService.php @@ -29,10 +29,17 @@ namespace VuFind\Db\Service; -use Laminas\Db\Sql\Select; +use Doctrine\ORM\Query\ResultSetMapping; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\ResourceTagsEntityInterface; use VuFind\Db\Entity\TagsEntityInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; +use VuFind\Db\Entity\UserResourceEntityInterface; +use VuFind\Log\LoggerAwareTrait; + +use function count; /** * Database service for tags. @@ -43,9 +50,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class TagService extends AbstractDbService implements TagServiceInterface, \VuFind\Db\Table\DbTableAwareInterface +class TagService extends AbstractDbService implements TagServiceInterface, DbServiceAwareInterface, LoggerAwareInterface { - use \VuFind\Db\Table\DbTableAwareTrait; + use DbServiceAwareTrait; + use Feature\ResourceSortTrait; + use LoggerAwareTrait; /** * Get statistics on use of tags. @@ -57,7 +66,18 @@ class TagService extends AbstractDbService implements TagServiceInterface, \VuFi */ public function getStatistics(bool $extended = false, bool $caseSensitiveTags = false): array { - return $this->getDbTable('ResourceTags')->getStatistics($extended, $caseSensitiveTags); + $dql = 'SELECT COUNT(DISTINCT(rt.user)) AS users, ' + . 'COUNT(DISTINCT(rt.resource)) AS resources, ' + . 'COUNT(rt.id) AS total ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + $resourceTagsService = $this->getDbService(ResourceTagsServiceInterface::class); + if ($extended) { + $stats['unique'] = count($resourceTagsService->getUniqueTags(caseSensitive: $caseSensitiveTags)); + $stats['anonymous'] = $resourceTagsService->getAnonymousCount(); + } + return $stats; } /** @@ -76,7 +96,9 @@ public function getNonListTagsFuzzilyMatchingString( int $limit = 100, bool $caseSensitive = false ): array { - return $this->getDbTable('Tags')->matchText($text, $sort, $limit, $caseSensitive); + $where = ['LOWER(t.tag) LIKE LOWER(:text)', 'rt.resource is NOT NULL ']; + $parameters = ['text' => $text . '%']; + return $this->getTagListWithDoctrine($sort, $limit, $where, $parameters, $caseSensitive); } /** @@ -91,14 +113,11 @@ public function getNonListTagsFuzzilyMatchingString( */ public function getTagsByText(string $text, bool $caseSensitive = false): array { - $callback = function ($select) use ($text, $caseSensitive) { - if ($caseSensitive) { - $select->where->equalTo('tag', $text); - } else { - $select->where->literal('lower(tag) = lower(?)', [$text]); - } - }; - return iterator_to_array($this->getDbTable('Tags')->select($callback)); + $dql = 'SELECT t FROM ' . TagsEntityInterface::class . ' t ' + . ($caseSensitive ? 'WHERE t.tag=:tag' : 'WHERE LOWER(t.tag) = LOWER(:tag)'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters(['tag' => $text]); + return $query->getResult(); } /** @@ -115,6 +134,56 @@ public function getTagByText(string $text, bool $caseSensitive = false): ?TagsEn return $tags[0] ?? null; } + /** + * Get a list of tags based on a sort method ($sort) and a where clause. + * + * @param string $sort Sort/search parameter + * @param int $limit Maximum number of tags (default = 100, < 1 = no limit) + * @param array $where Array of where clauses + * @param array $parameters Array of query parameters + * @param bool $caseSensitive Should tags be retrieved case-sensitively? + * + * @return array Tag details. + */ + protected function getTagListWithDoctrine( + string $sort = 'alphabetical', + int $limit = 100, + array $where = [], + array $parameters = [], + bool $caseSensitive = false + ) { + $tagClause = $caseSensitive ? 't.tag' : 'LOWER(t.tag)'; + $dql = 'SELECT t.id as id, COUNT(DISTINCT(rt.resource)) as cnt, MAX(rt.posted) as posted, ' + . $tagClause . ' AS tag ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'JOIN rt.tag t '; + if (!empty($where)) { + $dql .= ' WHERE ' . implode(' AND ', $where) . ' '; + } + + $dql .= 'GROUP BY t.id, t.tag '; + $dql .= match ($sort) { + 'alphabetical' => 'ORDER BY lower(t.tag), cnt DESC ', + 'popularity' => 'ORDER BY cnt DESC, lower(t.tag) ', + 'recent' => 'ORDER BY posted DESC, cnt DESC, lower(t.tag) ', + default => '', + }; + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->setMaxResults($limit); + $results = $query->getResult(); + + $tagList = []; + foreach ($results as $result) { + $tagList[] = [ + 'tag' => $result['tag'], + 'cnt' => $result['cnt'], + ]; + } + return $tagList; + } + /** * Get all resources associated with the provided tag query. * @@ -137,17 +206,107 @@ public function getResourcesMatchingTagQuery( bool $fuzzy = true, bool $caseSensitive = false ): array { - return iterator_to_array( - $this->getDbTable('Tags')->resourceSearch( - $q, - $source, - $sort, - $offset, - $limit, - $fuzzy, - $caseSensitive - ) - ); + $orderByDetails = empty($sort) ? [] : $this->getResourceOrderByClause($sort); + $dql = 'SELECT DISTINCT(r.id) AS resource, r'; + if (!empty($orderByDetails['extraSelect'])) { + $dql .= ', ' . $orderByDetails['extraSelect']; + } + $dql .= ' FROM ' . TagsEntityInterface::class . ' t ' + . 'JOIN ' . ResourceTagsEntityInterface::class . ' rt WITH t.id = rt.tag ' + . 'JOIN ' . ResourceEntityInterface::class . ' r WITH r.id = rt.resource ' + . 'WHERE rt.resource IS NOT NULL '; + $parameters = compact('q'); + if ($fuzzy) { + $dql .= 'AND LOWER(t.tag) LIKE LOWER(:q) '; + } elseif (!$caseSensitive) { + $dql .= 'AND LOWER(t.tag) = LOWER(:q) '; + } else { + $dql .= 'AND t.tag = :q '; + } + + if (!empty($source)) { + $dql .= 'AND r.source = :source'; + $parameters['source'] = $source; + } + + if (!empty($orderByDetails['orderByClause'])) { + $dql .= $orderByDetails['orderByClause']; + } + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + if ($offset > 0) { + $query->setFirstResult($offset); + } + if (null !== $limit) { + $query->setMaxResults($limit); + } + $results = $query->getResult(); + return $results; + } + + /** + * Support method for other getRecordTags*() methods to consolidate shared logic. + * + * @param string $id Record ID to look up + * @param string $source Source of record to look up + * @param int $limit Max. number of tags to return (0 = no limit) + * @param UserEntityInterface|int|null $userOrId ID of user to load tags from (null for all users) + * @param string $sort Sort type ('count' or 'tag') + * @param UserEntityInterface|int|null $ownerOrId ID of user to check for ownership + * @param array $extraWhereClauses Extra where clauses to apply to query + * @param array $extraParameters Extra parameters to provide with query + * @param bool $caseSensitive Should tags be treated case-sensitively? + * + * @return array + */ + protected function getRecordTagsWithDoctrine( + string $id, + string $source = DEFAULT_SEARCH_BACKEND, + int $limit = 0, + UserEntityInterface|int|null $userOrId = null, + string $sort = 'count', + UserEntityInterface|int|null $ownerOrId = null, + array $extraWhereClauses = [], + array $extraParameters = [], + bool $caseSensitive = false + ): array { + $parameters = compact('id', 'source') + $extraParameters; + $tag = $caseSensitive ? 't.tag' : 'lower(t.tag)'; + $fieldList = 't.id AS id, COUNT(DISTINCT(rt.user)) AS cnt, ' . $tag . ' AS tag'; + // If we're looking for ownership, adjust query to include an "is_me" flag value indicating + // if the selected resource is tagged by the specified user. + if (!empty($ownerOrId)) { + $fieldList .= ', MAX(CASE WHEN rt.user = :userToCheck THEN 1 ELSE 0 END) AS is_me'; + $parameters['userToCheck'] = $this->getDoctrineReference(UserEntityInterface::class, $ownerOrId); + } + $dql = 'SELECT ' . $fieldList . ' FROM ' . TagsEntityInterface::class . ' t ' + . 'JOIN ' . ResourceTagsEntityInterface::class . ' rt WITH t.id = rt.tag ' + . 'JOIN ' . ResourceEntityInterface::class . ' r WITH r.id = rt.resource ' + . 'WHERE r.recordId = :id AND r.source = :source '; + + foreach ($extraWhereClauses as $clause) { + $dql .= "AND $clause "; + } + + if (null !== $userOrId) { + $dql .= 'AND rt.user = :user '; + $parameters['user'] = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + } + + $dql .= 'GROUP BY t.id, t.tag '; + if ($sort == 'count') { + $dql .= 'ORDER BY cnt DESC, LOWER(t.tag) '; + } elseif ($sort == 'tag') { + $dql .= 'ORDER BY LOWER(t.tag) '; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + if ($limit > 0) { + $query->setMaxResults($limit); + } + $results = $query->getResult(); + return $results; } /** @@ -161,11 +320,9 @@ public function getResourcesMatchingTagQuery( */ public function getTagBrowseList(string $sort, int $limit, bool $caseSensitive = false): array { - $callback = function ($select) { - // Discard user list tags - $select->where->isNotNull('resource_tags.resource_id'); - }; - return $this->getDbTable('Tags')->getTagList($sort, $limit, $callback, $caseSensitive); + // Extra where clause is to discard user list tags: + return $this + ->getTagListWithDoctrine($sort, $limit, ['rt.resource is NOT NULL'], caseSensitive: $caseSensitive); } /** @@ -192,12 +349,22 @@ public function getRecordTags( UserEntityInterface|int|null $ownerOrId = null, bool $caseSensitive = false ): array { - $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId; - return $this->getDbTable('Tags') - ->getForResource($id, $source, $limit, $listId, $userId, $sort, $userToCheck, $caseSensitive) - ->toArray(); + $extraClauses = $extraParams = []; + if ($listOrId) { + $extraClauses[] = 'rt.list = :list'; + $extraParams['list'] = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + } + return $this->getRecordTagsWithDoctrine( + $id, + $source, + $limit, + $userOrId, + $sort, + $ownerOrId, + $extraClauses, + $extraParams, + $caseSensitive + ); } /** @@ -226,12 +393,24 @@ public function getRecordTagsFromFavorites( UserEntityInterface|int|null $ownerOrId = null, bool $caseSensitive = false ): array { - $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId; - return $this->getDbTable('Tags') - ->getForResource($id, $source, $limit, $listId ?? true, $userId, $sort, $userToCheck, $caseSensitive) - ->toArray(); + $extraClauses = $extraParams = []; + if ($listOrId) { + $extraClauses[] = 'rt.list = :list'; + $extraParams['list'] = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + } else { + $extraClauses[] = 'rt.list IS NOT NULL'; + } + return $this->getRecordTagsWithDoctrine( + $id, + $source, + $limit, + $userOrId, + $sort, + $ownerOrId, + $extraClauses, + $extraParams, + $caseSensitive + ); } /** @@ -257,11 +436,58 @@ public function getRecordTagsNotInFavorites( UserEntityInterface|int|null $ownerOrId = null, bool $caseSensitive = false ): array { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $userToCheck = $ownerOrId instanceof UserEntityInterface ? $ownerOrId->getId() : $ownerOrId; - return $this->getDbTable('Tags') - ->getForResource($id, $source, $limit, false, $userId, $sort, $userToCheck, $caseSensitive) - ->toArray(); + return $this->getRecordTagsWithDoctrine( + $id, + $source, + $limit, + $userOrId, + $sort, + $ownerOrId, + ['rt.list IS NULL'], + [], + $caseSensitive + ); + } + + /** + * Merge source tag into target tag. + * + * @param TagsEntityInterface $target Target tag + * @param TagsEntityInterface $source Source tag + * + * @return void + */ + public function mergeTags(TagsEntityInterface $target, TagsEntityInterface $source): void + { + // Don't merge a tag with itself! + if ($target->getId() === $source->getId()) { + return; + } + + $result = $this->entityManager->getRepository(ResourceTagsEntityInterface::class) + ->findBy(['tag' => $source]); + + foreach ($result as $current) { + // Move the link to the target ID: + $this->getDbService(ResourceTagsServiceInterface::class)->createLink( + $current->getResource(), + $target, + $current->getUser(), + $current->getUserList(), + $current->getPosted() + ); + + // Remove the duplicate link: + $this->entityManager->remove($current); + } + // Remove the source tag: + $this->entityManager->remove($source); + try { + $this->entityManager->flush(); + } catch (\Exception $e) { + $this->logError('Clean up operation failed: ' . $e->getMessage()); + throw $e; + } } /** @@ -274,7 +500,17 @@ public function getRecordTagsNotInFavorites( */ public function getDuplicateTags(bool $caseSensitive = false): array { - return $this->getDbTable('Tags')->getDuplicates($caseSensitive)->toArray(); + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('tag', 'tag'); + $rsm->addScalarResult('cnt', 'cnt'); + $rsm->addScalarResult('id', 'id'); + $sql = 'SELECT MIN(tag) AS tag, COUNT(tag) AS cnt, MIN(id) AS id ' + . 'FROM tags t ' + . 'GROUP BY ' . ($caseSensitive ? 't.tag ' : 'LOWER(t.tag) ') + . 'HAVING COUNT(tag) > 1'; + $statement = $this->entityManager->createNativeQuery($sql, $rsm); + $results = $statement->getResult(); + return $results; } /** @@ -301,8 +537,32 @@ public function getUserTagsFromFavorites( ): array { $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - return $this->getDbTable('Tags')->getListTagsForUser($userId, $recordId, $listId, $source, $caseSensitive) - ->toArray(); + $tag = $caseSensitive ? 't.tag' : 'lower(t.tag)'; + $dql = 'SELECT MIN(t.id) AS id, ' . $tag . ' AS tag, COUNT(DISTINCT(rt.resource)) AS cnt ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'JOIN rt.tag t ' + . 'JOIN rt.resource r ' + . 'JOIN ' . UserResourceEntityInterface::class . ' ur ' + . 'WITH r.id = ur.resource ' + . 'WHERE ur.user = :userId AND rt.user = :userId AND ur.list = rt.list '; + $parameters = compact('userId'); + if (null !== $source) { + $dql .= 'AND r.source = :source '; + $parameters['source'] = $source; + } + if (null !== $recordId) { + $dql .= 'AND r.recordId = :recordId '; + $parameters['recordId'] = $recordId; + } + if (null !== $listId) { + $dql .= 'AND rt.list = :listId '; + $parameters['listId'] = $listId; + } + $dql .= 'GROUP BY t.tag ORDER BY LOWER(t.tag) '; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -320,8 +580,24 @@ public function getListTags( $caseSensitive = false ): array { $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - return $this->getDbTable('Tags')->getForList($listId, $userId, $caseSensitive)->toArray(); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $tag = $caseSensitive ? 't.tag' : 'lower(t.tag)'; + + $dql = 'SELECT MIN(t.id) AS id, ' . $tag . ' AS tag ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'JOIN rt.tag t ' + . 'WHERE rt.list = :listId AND rt.resource IS NULL '; + $parameters = compact('listId'); + if ($user) { + $dql .= 'AND rt.user = :userId '; + $parameters['userId'] = $user; + } + + $dql .= 'GROUP BY t.tag ORDER BY LOWER(t.tag) '; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -331,15 +607,11 @@ public function getListTags( */ public function deleteOrphanedTags(): void { - $callback = function ($select) { - $subQuery = $this->getDbTable('ResourceTags') - ->getSql() - ->select() - ->quantifier(Select::QUANTIFIER_DISTINCT) - ->columns(['tag_id']); - $select->where->notIn('id', $subQuery); - }; - $this->getDbTable('Tags')->delete($callback); + $dql = 'DELETE FROM ' . TagsEntityInterface::class . ' t ' + . 'WHERE t NOT IN (SELECT IDENTITY(rt.tag) FROM ' + . ResourceTagsEntityInterface::class . ' rt)'; + $query = $this->entityManager->createQuery($dql); + $query->execute(); } /** @@ -351,7 +623,7 @@ public function deleteOrphanedTags(): void */ public function getTagById(int $id): ?TagsEntityInterface { - return $this->getDbTable('Tags')->select(['id' => $id])->current(); + return $this->entityManager->find(TagsEntityInterface::class, $id); } /** @@ -361,6 +633,6 @@ public function getTagById(int $id): ?TagsEntityInterface */ public function createEntity(): TagsEntityInterface { - return $this->getDbTable('Tags')->createRow(); + return $this->entityPluginManager->get(TagsEntityInterface::class); } } diff --git a/module/VuFind/src/VuFind/Db/Service/TagServiceInterface.php b/module/VuFind/src/VuFind/Db/Service/TagServiceInterface.php index a19ce859e20..d812facdfdc 100644 --- a/module/VuFind/src/VuFind/Db/Service/TagServiceInterface.php +++ b/module/VuFind/src/VuFind/Db/Service/TagServiceInterface.php @@ -203,6 +203,16 @@ public function getRecordTagsNotInFavorites( bool $caseSensitive = false ): array; + /** + * Merge source tag into target tag. + * + * @param TagsEntityInterface $target Target tag + * @param TagsEntityInterface $source Source tag + * + * @return void + */ + public function mergeTags(TagsEntityInterface $target, TagsEntityInterface $source): void; + /** * Get a list of duplicate tags (this should never happen, but past bugs and the introduction of case-insensitive * tags have introduced problems). diff --git a/module/VuFind/src/VuFind/Db/Service/UserCardService.php b/module/VuFind/src/VuFind/Db/Service/UserCardService.php index e778262c5ca..041e1786477 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserCardService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserCardService.php @@ -31,12 +31,15 @@ namespace VuFind\Db\Service; use DateTime; +use Doctrine\ORM\EntityManager; +use Laminas\Log\LoggerAwareInterface; use VuFind\Auth\ILSAuthenticator; use VuFind\Config\AccountCapabilities; +use VuFind\Db\Entity\PluginManager as EntityPluginManager; use VuFind\Db\Entity\UserCardEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Db\PersistenceManager; +use VuFind\Log\LoggerAwareTrait; use function count; use function is_int; @@ -53,22 +56,29 @@ */ class UserCardService extends AbstractDbService implements DbServiceAwareInterface, - DbTableAwareInterface, + LoggerAwareInterface, UserCardServiceInterface { use DbServiceAwareTrait; - use DbTableAwareTrait; + use LoggerAwareTrait; /** * Constructor * - * @param ILSAuthenticator $ilsAuthenticator ILS authenticator - * @param AccountCapabilities $capabilities Account capabilities configuration + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager VuFind entity plugin manager + * @param PersistenceManager $persistenceManager Entity persistence manager + * @param ILSAuthenticator $ilsAuthenticator ILS authenticator + * @param AccountCapabilities $capabilities Account capabilities configuration */ public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, + PersistenceManager $persistenceManager, protected ILSAuthenticator $ilsAuthenticator, protected AccountCapabilities $capabilities ) { + parent::__construct($entityManager, $entityPluginManager, $persistenceManager); } /** @@ -78,7 +88,10 @@ public function __construct( */ public function getInsecureRows(): array { - return iterator_to_array($this->getDbTable('UserCard')->getInsecureRows()); + $dql = 'SELECT UC FROM ' . UserCardEntityInterface::class + . ' UC WHERE UC.catPassword IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + return $query->getResult(); } /** @@ -88,10 +101,10 @@ public function getInsecureRows(): array */ public function getAllRowsWithUsernames(): array { - $callback = function ($select) { - $select->where->isNotNull('cat_username'); - }; - return iterator_to_array($this->getDbTable('UserCard')->select($callback)); + $dql = 'SELECT UC FROM ' . UserCardEntityInterface::class + . ' UC WHERE UC.catUsername IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + return $query->getResult(); } /** @@ -111,19 +124,23 @@ public function getLibraryCards( if (!$this->capabilities->libraryCardsEnabled()) { return []; } - $callback = function ($select) use ($userOrId, $id, $catUsername) { - $select->where->equalTo('user_id', is_int($userOrId) ? $userOrId : $userOrId->getId()); - if ($id) { - $select->where->equalTo('id', $id); - } - if ($catUsername) { - $select->where->equalTo('cat_username', $catUsername); - } - // Sort by id for consistency and duplicate removal: - $select->order('id'); - }; - $userCard = $this->getDbTable('UserCard'); - return iterator_to_array($userCard->select($callback)); + $dql = 'SELECT UC FROM ' . UserCardEntityInterface::class . ' UC '; + $dqlWhere = ['UC.user = :user']; + $parameters['user'] = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + if (null !== $id) { + $dqlWhere[] = 'UC.id = :id'; + $parameters['id'] = $id; + } + if (null !== $catUsername) { + $dqlWhere[] = 'UC.catUsername = :catUsername'; + $parameters['catUsername'] = $catUsername; + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere) + . ' ORDER BY UC.id ASC'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $records = $query->getResult(); + return $records; } /** @@ -140,13 +157,10 @@ public function getOrCreateLibraryCard(UserEntityInterface|int $userOrId, ?int $ if (!$this->capabilities->libraryCardsEnabled()) { throw new \VuFind\Exception\LibraryCard('Library Cards Disabled'); } - if ($id === null) { - $user = is_int($userOrId) - ? $this->getDbService(UserServiceInterface::class)->getUserById($userOrId) : $userOrId; $row = $this->createEntity() ->setCardName('') - ->setUser($user) + ->setUser($this->getDoctrineReference(UserEntityInterface::class, $userOrId)) ->setCatUsername('') ->setRawCatPassword(''); } else { @@ -177,10 +191,13 @@ public function deleteLibraryCard(UserEntityInterface $user, UserCardEntityInter if (!$row) { throw new \Exception('Library card not found'); } - if (!$row instanceof \VuFind\Db\Row\UserCard) { - $row = $this->getDbTable('UserCard')->select(['id' => $cardId])->current(); + + try { + $this->deleteEntity($row); + } catch (\Exception $e) { + $this->logError('Could not delete UserCard: ' . $e->getMessage()); + return false; } - $row->delete(); if ($row->getCatUsername() == $user->getCatUsername()) { // Activate another card (if any) or remove cat_username and cat_password @@ -243,7 +260,7 @@ public function persistLibraryCardData( $row = ($id !== null) ? current($this->getLibraryCards($user, $id)) : null; if (empty($row)) { $row = $this->createEntity() - ->setUser($user) + ->setUser($this->getDoctrineReference(UserEntityInterface::class, $user)) ->setCreated(new DateTime()); } $row->setCardName($cardName); @@ -296,7 +313,7 @@ public function synchronizeUserLibraryCardData(UserEntityInterface|int $userOrId $cards = $this->getLibraryCards($user, catUsername: $user->getCatUsername()); if (!($card = reset($cards))) { $card = $this->createEntity() - ->setUser($user) + ->setUser($this->getDoctrineReference(UserEntityInterface::class, $user)) ->setCatUsername($user->getCatUsername()) ->setCardName($user->getCatUsername()) ->setCreated(new DateTime()); @@ -355,6 +372,6 @@ public function activateLibraryCard(UserEntityInterface|int $userOrId, int $id): */ public function createEntity(): UserCardEntityInterface { - return $this->getDbTable('UserCard')->createRow(); + return $this->entityPluginManager->get(UserCardEntityInterface::class); } } diff --git a/module/VuFind/src/VuFind/Db/Service/UserListService.php b/module/VuFind/src/VuFind/Db/Service/UserListService.php index 2767fb2e7a7..e57121b7345 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserListService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserListService.php @@ -31,16 +31,16 @@ namespace VuFind\Db\Service; use Exception; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\ExpressionInterface; -use Laminas\Db\Sql\Select; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\ResourceTagsEntityInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Db\Entity\UserResourceEntityInterface; use VuFind\Exception\RecordMissing as RecordMissingException; +use VuFind\Log\LoggerAwareTrait; -use function is_int; +use function count; /** * Database service for UserList. @@ -52,9 +52,13 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ -class UserListService extends AbstractDbService implements DbTableAwareInterface, UserListServiceInterface +class UserListService extends AbstractDbService implements + UserListServiceInterface, + LoggerAwareInterface, + DbServiceAwareInterface { - use DbTableAwareTrait; + use LoggerAwareTrait; + use DbServiceAwareTrait; /** * Create a UserList entity object. @@ -63,7 +67,7 @@ class UserListService extends AbstractDbService implements DbTableAwareInterface */ public function createEntity(): UserListEntityInterface { - return $this->getDbTable('UserList')->createRow(); + return $this->entityPluginManager->get(UserListEntityInterface::class); } /** @@ -75,8 +79,7 @@ public function createEntity(): UserListEntityInterface */ public function deleteUserList(UserListEntityInterface|int $listOrId): void { - $listId = $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - $this->getDbTable('UserList')->delete(['id' => $listId]); + $this->deleteEntity($this->getDoctrineReference(UserListEntityInterface::class, $listOrId)); } /** @@ -89,7 +92,7 @@ public function deleteUserList(UserListEntityInterface|int $listOrId): void */ public function getUserListById(int $id): UserListEntityInterface { - $result = $this->getDbTable('UserList')->select(['id' => $id])->current(); + $result = $this->getEntityById(\VuFind\Db\Entity\UserList::class, $id); if (empty($result)) { throw new RecordMissingException('Cannot load list ' . $id); } @@ -106,21 +109,24 @@ public function getUserListById(int $id): UserListEntityInterface */ public function getPublicLists(array $includeFilter = [], array $excludeFilter = []): array { - $callback = function ($listOrId) { - return $listOrId instanceof UserListEntityInterface ? $listOrId->getId() : $listOrId; - }; - $includeIds = array_map($callback, $includeFilter); - $excludeIds = array_map($callback, $excludeFilter); - $callback = function ($select) use ($includeIds, $excludeIds) { - $select->where->equalTo('public', 1); - if ($excludeIds) { - $select->where->notIn('id', $excludeIds); - } - if ($includeIds) { - $select->where->in('id', $includeIds); - } - }; - return iterator_to_array($this->getDbTable('UserList')->select($callback)); + $dql = 'SELECT ul FROM ' . UserListEntityInterface::class . ' ul '; + + $parameters = []; + $where = ["ul.public = '1'"]; + if (!empty($includeFilter)) { + $where[] = 'ul IN (:includeFilter)'; + $parameters['includeFilter'] = $includeFilter; + } + if (!empty($excludeFilter)) { + $where[] = 'ul NOT IN (:excludeFilter)'; + $parameters['excludeFilter'] = $excludeFilter; + } + $dql .= 'WHERE ' . implode(' AND ', $where); + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -134,39 +140,18 @@ public function getPublicLists(array $includeFilter = [], array $excludeFilter = */ public function getUserListsAndCountsByUser(UserEntityInterface|int $userOrId): array { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function (Select $select) use ($userId) { - $select->columns( - [ - Select::SQL_STAR, - 'cnt' => new Expression( - 'COUNT(DISTINCT(?))', - ['ur.resource_id'], - [ExpressionInterface::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['ur' => 'user_resource'], - 'user_list.id = ur.list_id', - [], - $select::JOIN_LEFT - ); - $select->where->equalTo('user_list.user_id', $userId); - $select->group( - [ - 'user_list.id', 'user_list.user_id', 'title', 'description', - 'created', 'public', - ] - ); - $select->order(['title']); - }; - - $result = []; - foreach ($this->getDbTable('UserList')->select($callback) as $row) { - $result[] = ['list_entity' => $row, 'count' => $row->cnt]; - } - return $result; + $dql = 'SELECT ul AS list_entity, COUNT(DISTINCT(ur.resource)) AS count ' + . 'FROM ' . UserListEntityInterface::class . ' ul ' + . 'LEFT JOIN ' . UserResourceEntityInterface::class . ' ur WITH ur.list = ul.id ' + . 'WHERE ul.user = :user ' + . 'GROUP BY ul ' + . 'ORDER BY ul.title'; + + $parameters = ['user' => $this->getDoctrineReference(UserEntityInterface::class, $userOrId)]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -188,13 +173,48 @@ public function getUserListsByTagAndId( bool $andTags = true, bool $caseSensitiveTags = false ): array { - $lists = $this->getDbTable('ResourceTags') - ->getListsForTag($tag, $listId, $publicOnly, $andTags, $caseSensitiveTags); - $listIds = array_column(iterator_to_array($lists), 'list_id'); - $callback = function ($select) use ($listIds) { - $select->where->in('id', $listIds); - }; - return iterator_to_array($this->getDbTable('UserList')->select($callback)); + $tag = $tag ? (array)$tag : null; + $listId = $listId ? (array)$listId : null; + $dql = 'SELECT IDENTITY(rt.list) ' + . 'FROM ' . ResourceTagsEntityInterface::class . ' rt ' + . 'JOIN rt.tag t ' + . 'JOIN rt.list l ' + // Discard tags assigned to a user resource: + . 'WHERE rt.resource IS NULL ' + // Restrict to tags by list owner: + . 'AND rt.user = l.user '; + $parameters = []; + if (null !== $listId) { + $dql .= 'AND rt.list IN (:listId) '; + $parameters['listId'] = $listId; + } + if ($publicOnly) { + $dql .= "AND l.public = '1' "; + } + if ($tag) { + if ($caseSensitiveTags) { + $dql .= 'AND t.tag IN (:tag) '; + $parameters['tag'] = $tag; + } else { + $tagClauses = []; + foreach ($tag as $i => $currentTag) { + $tagPlaceholder = 'tag' . $i; + $tagClauses[] = 'LOWER(t.tag) = LOWER(:' . $tagPlaceholder . ')'; + $parameters[$tagPlaceholder] = $currentTag; + } + $dql .= 'AND (' . implode(' OR ', $tagClauses) . ')'; + } + } + $dql .= ' GROUP BY rt.list '; + if ($tag && $andTags) { + // If we are ANDing the tags together, only pick lists that match ALL tags: + $dql .= 'HAVING COUNT(DISTINCT(rt.tag)) = :cnt '; + $parameters['cnt'] = count(array_unique($tag)); + } + $dql .= 'ORDER BY rt.list'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $this->getUserListsById($query->getSingleColumnResult()); } /** @@ -206,12 +226,34 @@ public function getUserListsByTagAndId( */ public function getUserListsByUser(UserEntityInterface|int $userOrId): array { - $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $callback = function ($select) use ($userId) { - $select->where->equalTo('user_id', $userId); - $select->order(['title']); - }; - return iterator_to_array($this->getDbTable('UserList')->select($callback)); + $dql = 'SELECT ul ' + . 'FROM ' . UserListEntityInterface::class . ' ul ' + . 'WHERE ul.user = :user ' + . 'ORDER BY ul.title'; + + $parameters = ['user' => $this->getDoctrineReference(UserEntityInterface::class, $userOrId)]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; + } + + /** + * Retrieve a batch of list objects corresponding to the provided IDs + * + * @param int[] $ids List ids. + * + * @return array + */ + protected function getUserListsById(array $ids): array + { + $dql = 'SELECT ul FROM ' . UserListEntityInterface::class . ' ul ' + . 'WHERE ul.id IN (:ids)'; + $parameters = compact('ids'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -229,12 +271,22 @@ public function getListsContainingRecord( string $source = DEFAULT_SEARCH_BACKEND, UserEntityInterface|int|null $userOrId = null ): array { - return iterator_to_array( - $this->getDbTable('UserList')->getListsContainingResource( - $recordId, - $source, - is_int($userOrId) ? $userOrId : $userOrId->getId() - ) - ); + $dql = 'SELECT ul FROM ' . UserListEntityInterface::class . ' ul ' + . 'JOIN ' . UserResourceEntityInterface::class . ' ur WITH ur.list = ul.id ' + . 'JOIN ' . ResourceEntityInterface::class . ' r WITH r.id = ur.resource ' + . 'WHERE r.recordId = :recordId AND r.source = :source '; + + $parameters = compact('recordId', 'source'); + if (null !== $userOrId) { + $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; + $dql .= 'AND ur.user = :userId '; + $parameters['userId'] = $userId; + } + + $dql .= 'ORDER BY ul.title'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } } diff --git a/module/VuFind/src/VuFind/Db/Service/UserResourceService.php b/module/VuFind/src/VuFind/Db/Service/UserResourceService.php index 8461bc2a87b..c4585af4128 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserResourceService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserResourceService.php @@ -29,15 +29,13 @@ namespace VuFind\Db\Service; -use Exception; +use Laminas\Log\LoggerAwareInterface; use VuFind\Db\Entity\ResourceEntityInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; +use VuFind\Db\Entity\UserResource; use VuFind\Db\Entity\UserResourceEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; - -use function is_int; +use VuFind\Log\LoggerAwareTrait; /** * Database service for UserResource. @@ -49,12 +47,30 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class UserResourceService extends AbstractDbService implements - DbTableAwareInterface, + LoggerAwareInterface, DbServiceAwareInterface, UserResourceServiceInterface { + use LoggerAwareTrait; use DbServiceAwareTrait; - use DbTableAwareTrait; + + /** + * Get a list of duplicate rows (this sometimes happens after merging IDs, + * for example after a Summon resource ID changes). + * + * @return array + */ + public function getDuplicates() + { + $dql = 'SELECT MIN(ur.resource) as resource_id, MIN(ur.list) as list_id, ' + . 'MIN(ur.user) as user_id, COUNT(ur.resource) as cnt, MIN(ur.id) as id ' + . 'FROM ' . UserResourceEntityInterface::class . ' ur ' + . 'GROUP BY ur.resource, ur.list, ur.user ' + . 'HAVING COUNT(ur.resource) > 1'; + $query = $this->entityManager->createQuery($dql); + $result = $query->getResult(); + return $result; + } /** * Get information saved in a user's favorites for a particular record. @@ -74,11 +90,23 @@ public function getFavoritesForRecord( UserListEntityInterface|int|null $listOrId = null, UserEntityInterface|int|null $userOrId = null ): array { - $listId = is_int($listOrId) ? $listOrId : $listOrId?->getId(); - $userId = is_int($userOrId) ? $userOrId : $userOrId?->getId(); - return iterator_to_array( - $this->getDbTable('UserResource')->getSavedData($recordId, $source, $listId, $userId) - ); + $dql = 'SELECT DISTINCT ur FROM ' . UserResourceEntityInterface::class . ' ur ' + . 'JOIN ' . ResourceEntityInterface::class . ' r WITH r.id = ur.resource ' + . 'WHERE r.source = :source AND r.recordId = :recordId '; + + $parameters = compact('source', 'recordId'); + if (null !== $userOrId) { + $dql .= 'AND ur.user = :user '; + $parameters['user'] = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + } + if (null !== $listOrId) { + $dql .= 'AND ur.list = :list'; + $parameters['list'] = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + } + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getResult(); } /** @@ -88,7 +116,14 @@ public function getFavoritesForRecord( */ public function getStatistics(): array { - return $this->getDbTable('UserResource')->getStatistics(); + $dql = 'SELECT COUNT(DISTINCT(u.user)) AS users, ' + . 'COUNT(DISTINCT(u.list)) AS lists, ' + . 'COUNT(DISTINCT(u.resource)) AS resources, ' + . 'COUNT(u.id) AS total ' + . 'FROM ' . UserResourceEntityInterface::class . ' u'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -107,27 +142,14 @@ public function createOrUpdateLink( UserListEntityInterface|int $listOrId, string $notes = '' ): UserResourceEntityInterface { - $resource = $resourceOrId instanceof ResourceEntityInterface - ? $resourceOrId : $this->getDbService(ResourceServiceInterface::class)->getResourceById($resourceOrId); - if (!$resource) { - throw new Exception("Cannot retrieve resource $resourceOrId"); - } - $list = $listOrId instanceof UserListEntityInterface - ? $listOrId : $this->getDbService(UserListServiceInterface::class)->getUserListById($listOrId); - if (!$list) { - throw new Exception("Cannot retrieve list $listOrId"); - } - $user = $userOrId instanceof UserEntityInterface - ? $userOrId : $this->getDbService(UserServiceInterface::class)->getUserById($userOrId); - if (!$user) { - throw new Exception("Cannot retrieve user $userOrId"); - } - $params = [ - 'resource_id' => $resource->getId(), - 'list_id' => $list->getId(), - 'user_id' => $user->getId(), - ]; - if (!($result = $this->getDbTable('UserResource')->select($params)->current())) { + $resource = $this->getDoctrineReference(ResourceEntityInterface::class, $resourceOrId); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $list = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + $params = compact('resource', 'list', 'user'); + $result = current($this->entityManager->getRepository(UserResourceEntityInterface::class) + ->findBy($params)); + + if (empty($result)) { $result = $this->createEntity() ->setResource($resource) ->setUser($user) @@ -155,21 +177,22 @@ public function unlinkFavorites( UserEntityInterface|int $userOrId, UserListEntityInterface|int|null $listOrId = null ): void { - // Build the where clause to figure out which rows to remove: - $listId = is_int($listOrId) ? $listOrId : $listOrId?->getId(); - $userId = is_int($userOrId) ? $userOrId : $userOrId->getId(); - $callback = function ($select) use ($resourceId, $userId, $listId) { - $select->where->equalTo('user_id', $userId); - if (null !== $resourceId) { - $select->where->in('resource_id', (array)$resourceId); - } - if (null !== $listId) { - $select->where->equalTo('list_id', $listId); - } - }; - - // Delete the rows: - $this->getDbTable('UserResource')->delete($callback); + $user = $this->getDoctrineReference(UserEntityInterface::class, $userOrId); + $dql = 'DELETE FROM ' . UserResourceEntityInterface::class . ' ur '; + $dqlWhere = ['ur.user = :user ']; + $parameters = compact('user'); + if (null !== $resourceId) { + $dqlWhere[] = ' ur.resource IN (:resource_id) '; + $parameters['resource_id'] = (array)$resourceId; + } + if (null !== $listOrId) { + $dqlWhere[] = ' ur.list = :list '; + $parameters['list'] = $this->getDoctrineReference(UserListEntityInterface::class, $listOrId); + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -179,7 +202,7 @@ public function unlinkFavorites( */ public function createEntity(): UserResourceEntityInterface { - return $this->getDbTable('UserResource')->createRow(); + return $this->entityPluginManager->get(UserResourceEntityInterface::class); } /** @@ -192,7 +215,12 @@ public function createEntity(): UserResourceEntityInterface */ public function changeResourceId(int $old, int $new): void { - $this->getDbTable('UserResource')->update(['resource_id' => $new], ['resource_id' => $old]); + $dql = 'UPDATE ' . UserResourceEntityInterface::class . ' e ' + . 'SET e.resource = :new WHERE e.resource = :old'; + $parameters = compact('new', 'old'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -202,6 +230,47 @@ public function changeResourceId(int $old, int $new): void */ public function deduplicate(): void { - $this->getDbTable('UserResource')->deduplicate(); + $repo = $this->entityManager->getRepository(UserResourceEntityInterface::class); + foreach ($this->getDuplicates() as $dupe) { + // Do this as a transaction to prevent odd behavior: + $this->entityManager->getConnection()->beginTransaction(); + + // Merge notes together... + $mainCriteria = [ + 'resource' => $dupe['resource_id'], + 'list' => $dupe['list_id'], + 'user' => $dupe['user_id'], + ]; + try { + $dupeRows = $repo->findBy($mainCriteria); + $notes = []; + foreach ($dupeRows as $row) { + if (!empty($row->getNotes())) { + $notes[] = $row->getNotes(); + } + } + $userResource = $this->getDoctrineReference(UserResourceEntityInterface::class, $dupe['id']); + $userResource->setNotes(implode(' ', $notes)); + $this->entityManager->flush(); + + // Now delete extra rows... + // match on all relevant IDs in duplicate group + // getDuplicates returns the minimum id in the set, so we want to + // delete all of the duplicates with a higher id value. + $dql = 'DELETE FROM ' . UserResourceEntityInterface::class . ' ur ' + . 'WHERE ur.resource = :resource AND ur.list = :list ' + . 'AND ur.user = :user AND ur.id > :id'; + $mainCriteria['id'] = $dupe['id']; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($mainCriteria); + $query->execute(); + // Done -- commit the transaction: + $this->entityManager->getConnection()->commit(); + } catch (\Exception $e) { + // If something went wrong, roll back the transaction and rethrow the error: + $this->entityManager->getConnection()->rollBack(); + throw $e; + } + } } } diff --git a/module/VuFind/src/VuFind/Db/Service/UserService.php b/module/VuFind/src/VuFind/Db/Service/UserService.php index e76af11a511..1fd28cc2464 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserService.php @@ -29,14 +29,14 @@ namespace VuFind\Db\Service; -use Laminas\Log\LoggerAwareInterface; +use DateTime; +use Doctrine\ORM\EntityManager; use Laminas\Session\Container as SessionContainer; use VuFind\Auth\UserSessionPersistenceInterface; +use VuFind\Db\Entity\PluginManager as EntityPluginManager; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Row\User as UserRow; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; -use VuFind\Log\LoggerAwareTrait; +use VuFind\Db\PersistenceManager; /** * Database service for user. @@ -48,21 +48,34 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class UserService extends AbstractDbService implements - DbTableAwareInterface, - LoggerAwareInterface, UserServiceInterface, UserSessionPersistenceInterface { - use DbTableAwareTrait; - use LoggerAwareTrait; - /** * Constructor * - * @param SessionContainer $userSessionContainer Session container for user data + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager VuFind entity plugin manager + * @param PersistenceManager $persistenceManager Entity persistence manager + * @param SessionContainer $userSessionContainer Session container for user data */ - public function __construct(protected SessionContainer $userSessionContainer) + public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, + PersistenceManager $persistenceManager, + protected SessionContainer $userSessionContainer + ) { + parent::__construct($entityManager, $entityPluginManager, $persistenceManager); + } + + /** + * Create an access_token entity object. + * + * @return UserEntityInterface + */ + public function createEntity(): UserEntityInterface { + return $this->entityPluginManager->get(UserEntityInterface::class); } /** @@ -74,7 +87,11 @@ public function __construct(protected SessionContainer $userSessionContainer) */ public function createEntityForUsername(string $username): UserEntityInterface { - return $this->getDbTable('User')->createRowForUsername($username); + $user = $this->createEntity() + ->setUsername($username) + ->setCreated(new DateTime()) + ->setHasUserProvidedEmail(false); + return $user; } /** @@ -87,19 +104,29 @@ public function createEntityForUsername(string $username): UserEntityInterface public function deleteUser(UserEntityInterface|int $userOrId): void { $userId = $userOrId instanceof UserEntityInterface ? $userOrId->getId() : $userOrId; - $this->getDbTable('User')->delete(['id' => $userId]); + $dql = 'DELETE FROM ' . UserEntityInterface::class . ' u' + . ' WHERE u.id = :id'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('id', $userId); + $query->execute(); } /** * Retrieve a user object from the database based on ID. * - * @param int $id ID. + * @param int $id ID value. * * @return ?UserEntityInterface */ public function getUserById(int $id): ?UserEntityInterface { - return $this->getDbTable('User')->getById($id); + $dql = 'SELECT u ' + . 'FROM ' . UserEntityInterface::class . ' u ' + . 'WHERE u.id = :id'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('id', $id); + $result = $query->getOneOrNullResult(); + return $result; } /** @@ -113,17 +140,31 @@ public function getUserById(int $id): ?UserEntityInterface */ public function getUserByField(string $fieldName, int|string|null $fieldValue): ?UserEntityInterface { - switch ($fieldName) { - case 'email': - return $this->getDbTable('User')->getByEmail($fieldValue); - case 'id': - return $this->getDbTable('User')->getById($fieldValue); - case 'username': - return $this->getDbTable('User')->getByUsername($fieldValue, false); - case 'verify_hash': - return $this->getDbTable('User')->getByVerifyHash($fieldValue); - case 'cat_id': - return $this->getDbTable('User')->getByCatalogId($fieldValue); + // Null ID lookups cannot possibly retrieve a value: + if ($fieldName === 'id' && $fieldValue === null) { + return null; + } + // Map expected incoming values (actual database columns) to legal values (Doctrine properties) + $legalFieldMap = [ + 'id' => 'id', + 'username' => 'username', + 'email' => 'email', + 'cat_id' => 'catId', + 'verify_hash' => 'verifyHash', + ]; + // For now, only username lookups are case-insensitive: + $caseInsensitive = $fieldName === 'username'; + if (isset($legalFieldMap[$fieldName])) { + $where = $caseInsensitive + ? 'LOWER(U.' . $legalFieldMap[$fieldName] . ') = LOWER(:fieldValue)' + : 'U.' . $legalFieldMap[$fieldName] . ' = :fieldValue'; + $dql = 'SELECT U FROM ' . UserEntityInterface::class . ' U ' + . 'WHERE ' . $where; + $parameters = compact('fieldValue'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = current($query->getResult()); + return $result ?: null; } throw new \InvalidArgumentException('Field name must be id, username, email or cat_id'); } @@ -212,11 +253,7 @@ public function updateUserEmail( */ public function addUserDataToSession(UserEntityInterface $user): void { - if ($user instanceof UserRow) { - $this->userSessionContainer->userDetails = $user->toArray(); - } else { - throw new \Exception($user::class . ' not supported by addUserDataToSession()'); - } + $this->userSessionContainer->userDetails = $user->toArray(); } /** @@ -280,10 +317,12 @@ public function hasUserSessionData(): bool */ public function getAllUsersWithCatUsernames(): array { - $callback = function ($select) { - $select->where->isNotNull('cat_username'); - }; - return iterator_to_array($this->getDbTable('User')->select($callback)); + $dql = 'SELECT u ' + . 'FROM ' . UserEntityInterface::class . ' u ' + . 'WHERE u.catUsername IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + $result = $query->getResult(); + return $result; } /** @@ -293,16 +332,12 @@ public function getAllUsersWithCatUsernames(): array */ public function getInsecureRows(): array { - return iterator_to_array($this->getDbTable('User')->getInsecureRows()); - } - - /** - * Create a new user entity. - * - * @return UserEntityInterface - */ - public function createEntity(): UserEntityInterface - { - return $this->getDbTable('User')->createRow(); + $dql = 'SELECT u ' + . 'FROM ' . UserEntityInterface::class . ' u ' + . "WHERE u.password != '' " + . 'AND u.catPassword IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + $result = $query->getResult(); + return $result; } } diff --git a/module/VuFind/src/VuFind/Db/Table/AccessToken.php b/module/VuFind/src/VuFind/Db/Table/AccessToken.php deleted file mode 100644 index 27124a36243..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/AccessToken.php +++ /dev/null @@ -1,142 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\AccessToken as AccessTokenRow; -use VuFind\Db\Row\RowGateway; - -/** - * Table Definition for access_token - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class AccessToken extends Gateway -{ - use ExpirationTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'access_token' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Retrieve an object from the database based on id and type; create a new - * row if no existing match is found. - * - * @param string $id Token ID - * @param string $type Token type - * @param bool $create Should we create rows that don't already exist? - * - * @return ?AccessTokenRow - */ - public function getByIdAndType( - string $id, - string $type, - bool $create = true - ): ?AccessTokenRow { - $row = $this->select(['id' => $id, 'type' => $type])->current(); - if ($create && empty($row)) { - $row = $this->createRow(); - $row->id = $id; - $row->type = $type; - $row->created = date('Y-m-d H:i:s'); - } - return $row; - } - - /** - * Add or replace an OpenID nonce for a user - * - * @param int $userId User ID - * @param ?string $nonce Nonce - * - * @return void - */ - public function storeNonce(int $userId, ?string $nonce) - { - $row = $this->getByIdAndType($userId, 'openid_nonce'); - $row->created = date('Y-m-d H:i:s'); - $row->user_id = $userId; - $row->data = json_encode(compact('nonce')); - $row->save(); - } - - /** - * Retrieve an OpenID nonce for a user - * - * @param int $userId User ID - * - * @return ?string - */ - public function getNonce(int $userId): ?string - { - if ($row = $this->getByIdAndType($userId, 'openid_nonce', false)) { - $data = json_decode($row->data, true); - return $data['nonce'] ?? null; - } - return null; - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - protected function expirationCallback($select, $dateLimit) - { - $select->where->lessThan('created', $dateLimit); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/AuthHash.php b/module/VuFind/src/VuFind/Db/Table/AuthHash.php deleted file mode 100644 index 98f10f8fe3e..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/AuthHash.php +++ /dev/null @@ -1,123 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; - -/** - * Table Definition for auth_hash - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class AuthHash extends Gateway -{ - use ExpirationTrait; - - public const TYPE_EMAIL = 'email'; // EmailAuthenticator - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'auth_hash' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Retrieve an object from the database based on hash and type; create a new - * row if no existing match is found. - * - * @param string $hash Hash - * @param string $type Hash type - * @param bool $create Should we create rows that don't already exist? - * - * @return ?\VuFind\Db\Row\AuthHash - */ - public function getByHashAndType($hash, $type, $create = true) - { - $row = $this->select(['hash' => $hash, 'type' => $type])->current(); - if ($create && empty($row)) { - $row = $this->createRow(); - $row->hash = $hash; - $row->type = $type; - $row->created = date('Y-m-d H:i:s'); - } - return $row; - } - - /** - * Retrieve last object from the database based on session id. - * - * @param string $sessionId Session ID - * - * @return ?\VuFind\Db\Row\AuthHash - */ - public function getLatestBySessionId($sessionId) - { - $callback = function ($select) use ($sessionId) { - $select->where->equalTo('session_id', $sessionId); - $select->order('created DESC'); - }; - return $this->select($callback)->current(); - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - protected function expirationCallback($select, $dateLimit) - { - $select->where->lessThan('created', $dateLimit); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php b/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php deleted file mode 100644 index d660fc01c88..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php +++ /dev/null @@ -1,327 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use VuFind\Db\Row\RowGateway; - -/** - * Table Definition for change_tracker - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class ChangeTracker extends Gateway -{ - /** - * Date/time format for database - * - * @var string - */ - protected $dateFormat = 'Y-m-d H:i:s'; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'change_tracker' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Retrieve a row from the database based on primary key; return null if it - * is not found. - * - * @param string $core The Solr core holding the record. - * @param string $id The ID of the record being indexed. - * - * @return ?\VuFind\Db\Row\ChangeTracker - */ - public function retrieve($core, $id) - { - return $this->select(['core' => $core, 'id' => $id])->current(); - } - - /** - * Build a callback function for use by the retrieveDeleted* methods. - * - * @param string $core The Solr core holding the record. - * @param string $from The beginning date of the range to search. - * @param string $until The end date of the range to search. - * @param int $offset Record number to retrieve first. - * @param int $limit Retrieval limit (null for no limit) - * @param array $columns Columns to retrieve (null for all) - * @param string $order Sort order - * - * @return callable - */ - public function getRetrieveDeletedCallback( - $core, - $from, - $until, - $offset = 0, - $limit = null, - $columns = null, - $order = null - ) { - return function ($select) use ( - $core, - $from, - $until, - $offset, - $limit, - $columns, - $order - ) { - if ($columns !== null) { - $select->columns($columns); - } - $select->where - ->equalTo('core', $core) - ->greaterThanOrEqualTo('deleted', $from) - ->lessThanOrEqualTo('deleted', $until); - if ($order !== null) { - $select->order($order); - } - if ($offset > 0) { - $select->offset($offset); - } - if ($limit !== null) { - $select->limit($limit); - } - }; - } - - /** - * Retrieve a set of deleted rows from the database. - * - * @param string $core The Solr core holding the record. - * @param string $from The beginning date of the range to search. - * @param string $until The end date of the range to search. - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function retrieveDeletedCount($core, $from, $until) - { - $columns = ['count' => new Expression('COUNT(*)')]; - $callback = $this - ->getRetrieveDeletedCallback($core, $from, $until, 0, null, $columns); - $select = $this->sql->select(); - $callback($select); - $statement = $this->sql->prepareStatementForSqlObject($select); - $result = $statement->execute(); - return ((array)$result->current())['count']; - } - - /** - * Retrieve a set of deleted rows from the database. - * - * @param string $core The Solr core holding the record. - * @param string $from The beginning date of the range to search. - * @param string $until The end date of the range to search. - * @param int $offset Record number to retrieve first. - * @param int $limit Retrieval limit (null for no limit) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function retrieveDeleted( - $core, - $from, - $until, - $offset = 0, - $limit = null - ) { - $callback = $this->getRetrieveDeletedCallback( - $core, - $from, - $until, - $offset, - $limit, - null, - 'deleted' - ); - return $this->select($callback); - } - - /** - * Retrieve a row from the database based on primary key; create a new - * row if no existing match is found. - * - * @param string $core The Solr core holding the record. - * @param string $id The ID of the record being indexed. - * - * @return \VuFind\Db\Row\ChangeTracker - */ - public function retrieveOrCreate($core, $id) - { - $row = $this->retrieve($core, $id); - if (empty($row)) { - $row = $this->createRow(); - $row->core = $core; - $row->id = $id; - $row->first_indexed = $row->last_indexed = $this->getUtcDate(); - } - return $row; - } - - /** - * Update the change tracker table to indicate that a record has been deleted. - * - * The method returns the updated/created row when complete. - * - * @param string $core The Solr core holding the record. - * @param string $id The ID of the record being indexed. - * - * @return \VuFind\Db\Row\ChangeTracker - */ - public function markDeleted($core, $id) - { - // Get a row matching the specified details: - $row = $this->retrieveOrCreate($core, $id); - - // If the record is already deleted, we don't need to do anything! - if (!empty($row->deleted)) { - return $row; - } - - // Save new value to the object: - $row->deleted = $this->getUtcDate(); - $row->save(); - return $row; - } - - /** - * Get a UTC time. - * - * @param int $ts Timestamp (null for current) - * - * @return string - */ - protected function getUtcDate($ts = null) - { - $oldTz = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $date = date($this->dateFormat, $ts ?? time()); - date_default_timezone_set($oldTz); - return $date; - } - - /** - * Convert a string to time in UTC. - * - * @param string $str String to parse - * - * @return int - */ - protected function strToUtcTime($str) - { - $oldTz = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $time = strtotime($str); - date_default_timezone_set($oldTz); - return $time; - } - - /** - * Update the change_tracker table to reflect that a record has been indexed. - * We need to know the date of the last change to the record (independent of - * its addition to the index) in order to tell the difference between a - * reindex of a previously-encountered record and a genuine change. - * - * The method returns the updated/created row when complete. - * - * @param string $core The Solr core holding the record. - * @param string $id The ID of the record being indexed. - * @param int $change The timestamp of the last record change. - * - * @return \VuFind\Db\Row\ChangeTracker - */ - public function index($core, $id, $change) - { - // Get a row matching the specified details: - $row = $this->retrieveOrCreate($core, $id); - - // Flag to indicate whether we need to save the contents of $row: - $saveNeeded = false; - - // Make sure there is a change date in the row (this will be empty - // if we just created a new row): - if (empty($row->last_record_change)) { - $row->last_record_change = $this->getUtcDate($change); - $saveNeeded = true; - } - - // Are we restoring a previously deleted record, or was the stored - // record change date before current record change date? Either way, - // we need to update the table! - if ( - !empty($row->deleted) - || $this->strToUtcTime($row->last_record_change) < $change - ) { - // Save new values to the object: - $row->last_indexed = $this->getUtcDate(); - $row->last_record_change = $this->getUtcDate($change); - - // If first indexed is null, we're restoring a deleted record, so - // we need to treat it as new -- we'll use the current time. - if (empty($row->first_indexed)) { - $row->first_indexed = $row->last_indexed; - } - - // Make sure the record is "undeleted" if necessary: - $row->deleted = null; - - $saveNeeded = true; - } - - // Save the row if changes were made: - if ($saveNeeded) { - $row->save(); - } - - // Send back the row: - return $row; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Comments.php b/module/VuFind/src/VuFind/Db/Table/Comments.php deleted file mode 100644 index 39c8051a6b3..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Comments.php +++ /dev/null @@ -1,223 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; - -use function count; -use function is_object; - -/** - * Table Definition for comments - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Comments extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'comments' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get tags associated with the specified resource. - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * - * @return array|\Laminas\Db\ResultSet\AbstractResultSet - */ - public function getForResource($id, $source = DEFAULT_SEARCH_BACKEND) - { - $resourceService = $this->getDbService(ResourceServiceInterface::class); - $resource = $resourceService->getResourceByRecordId($id, $source); - if (!$resource) { - return []; - } - - $callback = function ($select) use ($resource) { - $select->columns([Select::SQL_STAR]); - $select->join( - ['u' => 'user'], - 'u.id = comments.user_id', - ['firstname', 'lastname'], - $select::JOIN_LEFT - ); - $select->where->equalTo('comments.resource_id', $resource->id); - $select->order('comments.created'); - }; - - return $this->select($callback); - } - - /** - * Delete a comment if the owner is logged in. Returns true on success. - * - * @param int $id ID of row to delete - * @param UserEntityInterface $user Logged in user object - * - * @return bool - */ - public function deleteIfOwnedByUser($id, $user) - { - // User must be object with ID: - if (!is_object($user) || !($userId = $user->getId())) { - return false; - } - - // Comment row must exist: - $matches = $this->select(['id' => $id]); - if (count($matches) == 0 || !($row = $matches->current())) { - return false; - } - - // Row must be owned by user: - if ($row->user_id != $userId) { - return false; - } - - // If we got this far, everything is okay: - $row->delete(); - return true; - } - - /** - * Deletes all comments by a user. - * - * @param \VuFind\Db\Row\User $user User object - * - * @return void - */ - public function deleteByUser($user) - { - $this->delete(['user_id' => $user->id]); - } - - /** - * Get statistics on use of comments. - * - * @return array - */ - public function getStatistics() - { - $select = $this->sql->select(); - $select->columns( - [ - 'users' => new Expression( - 'COUNT(DISTINCT(?))', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'resources' => new Expression( - 'COUNT(DISTINCT(?))', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'total' => new Expression('COUNT(*)'), - ] - ); - $statement = $this->sql->prepareStatementForSqlObject($select); - $result = $statement->execute(); - return (array)$result->current(); - } - - /** - * Get a paginated result of all comments made by the user. - * - * @param int $userId User ID - * @param int $limit Limit - * @param int $page Page - * @param string $sort Sort - * - * @return \Laminas\Paginator\Paginator - */ - public function getCommentsPaginator( - int $userId, - int $limit, - int $page, - string $sort - ): \Laminas\Paginator\Paginator { - $commentSelect = new Select(); - $commentSelect->from('comments') - ->where->equalTo('comments.user_id', $userId); - $commentSelect->columns([ - 'resource_id', - 'id', - 'comment', - 'user_id', - 'created', - ]) - ->join( - ['re' => 'resource'], - 'comments.resource_id = re.id', - ['record_id', 'source'], - Select::JOIN_LEFT - ); - $order = $sort ? $sort : 'created DESC'; - $commentSelect->order($order); - if ($page > 0) { - $commentSelect->offset($page); - } - $commentSelect->limit($limit); - - $adapter = new \Laminas\Paginator\Adapter\LaminasDb\DbSelect($commentSelect, $this->getSql()); - $paginator = new \Laminas\Paginator\Paginator($adapter); - $paginator->setItemCountPerPage($limit); - $paginator->setCurrentPageNumber($page); - return $paginator; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php b/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php deleted file mode 100644 index 11028032aff..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php +++ /dev/null @@ -1,87 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -/** - * Default implementation of DbTableAwareInterface. - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -trait DbTableAwareTrait -{ - /** - * Database table plugin manager - * - * @var \VuFind\Db\Table\PluginManager - */ - protected $tableManager; - - /** - * Get the table plugin manager. Throw an exception if it is missing. - * - * @throws \Exception - * @return \VuFind\Db\Table\PluginManager - */ - public function getDbTableManager() - { - if (null === $this->tableManager) { - throw new \Exception('DB table manager missing.'); - } - return $this->tableManager; - } - - /** - * Set the table plugin manager. - * - * @param \VuFind\Db\Table\PluginManager $manager Plugin manager - * - * @return void - */ - public function setDbTableManager(\VuFind\Db\Table\PluginManager $manager) - { - $this->tableManager = $manager; - } - - /** - * Get a database table object. - * - * @param string $table Table to load. - * - * @return Gateway - */ - public function getDbTable($table) - { - return $this->getDbTableManager()->get($table); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/ExpirationTrait.php b/module/VuFind/src/VuFind/Db/Table/ExpirationTrait.php deleted file mode 100644 index 4c6407587a2..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/ExpirationTrait.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Sql\Select; - -/** - * Trait for tables that support expiration - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -trait ExpirationTrait -{ - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - abstract protected function expirationCallback($select, $dateLimit); - - /** - * Delete expired records. Allows setting of 'from' and 'to' ID's so that rows - * can be deleted in small batches. - * - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * @param int|null $limit Maximum number of rows to delete or null for no - * limit. - * - * @return int Number of rows deleted - */ - public function deleteExpired($dateLimit, $limit = null) - { - // Determine the expiration parameters: - $lastId = $limit ? $this->getExpiredBatchLastId($dateLimit, $limit) : null; - $callback = function ($select) use ($dateLimit, $lastId) { - $this->expirationCallback($select, $dateLimit); - if (null !== $lastId) { - $select->where->and->lessThanOrEqualTo('id', $lastId); - } - }; - return $this->delete($callback); - } - - /** - * Get the highest id to delete in a batch. - * - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * @param int $limit Maximum number of rows to delete. - * - * @return int|null Highest id value to delete or null if a limiting id is not - * available - */ - protected function getExpiredBatchLastId($dateLimit, $limit) - { - // Determine the expiration date: - $callback = function ($select) use ($dateLimit, $limit) { - $this->expirationCallback($select, $dateLimit); - $select->columns(['id'])->order('id')->offset($limit - 1)->limit(1); - }; - $result = $this->select($callback)->current(); - return $result ? $result->id : null; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/ExternalSession.php b/module/VuFind/src/VuFind/Db/Table/ExternalSession.php deleted file mode 100644 index 9abe1c56868..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/ExternalSession.php +++ /dev/null @@ -1,132 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ExternalSessionServiceInterface; - -/** - * Table Definition for external_session - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class ExternalSession extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - use ExpirationTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'external_session' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Add a mapping between local and external session id's - * - * @param string $localSessionId Local (VuFind) session id - * @param string $externalSessionId External session id - * - * @return void - * - * @deprecated Use ExternalSessionServiceInterface::addSessionMapping() - */ - public function addSessionMapping($localSessionId, $externalSessionId) - { - $this->getDbService(ExternalSessionServiceInterface::class) - ->addSessionMapping($localSessionId, $externalSessionId); - } - - /** - * Retrieve an object from the database based on an external session ID - * - * @param string $sid External session ID to retrieve - * - * @return ?\VuFind\Db\Row\ExternalSession - * - * @deprecated Use ExternalSessionServiceInterface::getAllByExternalSessionId() - */ - public function getByExternalSessionId($sid) - { - $sessions = $this->getDbService(ExternalSessionServiceInterface::class)->getAllByExternalSessionId($sid); - return $sessions[0] ?? null; - } - - /** - * Destroy data for the given session ID. - * - * @param string $sid Session ID to erase - * - * @return void - * - * @deprecated Use ExternalSessionServiceInterface::destroySession() - */ - public function destroySession($sid) - { - $this->getDbService(ExternalSessionServiceInterface::class)->destroySession($sid); - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - protected function expirationCallback($select, $dateLimit) - { - $select->where->lessThan('created', $dateLimit); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Feedback.php b/module/VuFind/src/VuFind/Db/Table/Feedback.php deleted file mode 100644 index 924c0f593df..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Feedback.php +++ /dev/null @@ -1,133 +0,0 @@ - - * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -declare(strict_types=1); - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Paginator\Paginator; -use VuFind\Db\Row\RowGateway; - -use function intval; - -/** - * Class Feedback - * - * @category VuFind - * @package Db_Table - * @author Josef Moravec - * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Feedback extends Gateway -{ - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param RowGateway|null $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'feedback' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get feedback by filter - * - * @param string|null $formName Form name - * @param string|null $siteUrl Site URL - * @param string|null $status Current status - * @param string|null $page Current page - * @param int $limit Limit per page - * - * @return Paginator - */ - public function getFeedbackByFilter( - ?string $formName = null, - ?string $siteUrl = null, - ?string $status = null, - ?string $page = null, - int $limit = 20 - ): Paginator { - $sql = $this->getSql(); - $select = $sql->select(); - if (null !== $formName) { - $select->where->equalTo('form_name', $formName); - } - if (null !== $siteUrl) { - $select->where->equalTo('site_url', $siteUrl); - } - if (null !== $status) { - $select->where->equalTo('status', $status); - } - $select->order('created DESC'); - - $page = null === $page ? null : intval($page); - if (null !== $page) { - $select->limit($limit); - $select->offset($limit * ($page - 1)); - } - $adapter = new \Laminas\Paginator\Adapter\LaminasDb\DbSelect($select, $sql); - $paginator = new \Laminas\Paginator\Paginator($adapter); - $paginator->setItemCountPerPage($limit); - if (null !== $page) { - $paginator->setCurrentPageNumber($page); - } - return $paginator; - } - - /** - * Delete feedback by ids - * - * @param array $ids IDs - * - * @return int Count of deleted rows - */ - public function deleteByIdArray(array $ids): int - { - // Do nothing if we have no IDs to delete! - if (empty($ids)) { - return 0; - } - $callback = function ($select) use ($ids) { - $select->where->in('id', $ids); - }; - return $this->delete($callback); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Gateway.php b/module/VuFind/src/VuFind/Db/Table/Gateway.php deleted file mode 100644 index bec95c5e672..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Gateway.php +++ /dev/null @@ -1,191 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Exception; -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\TableGateway\AbstractTableGateway; -use Laminas\Db\TableGateway\Feature; -use VuFind\Db\Row\RowGateway; - -use function count; -use function is_object; - -/** - * Generic VuFind table gateway. - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Gateway extends AbstractTableGateway -{ - /** - * Table manager - * - * @var PluginManager - */ - protected $tableManager; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj, - $table - ) { - $this->adapter = $adapter; - $this->tableManager = $tm; - $this->table = $table; - - $this->initializeFeatures($cfg); - $this->initialize(); - - if (null !== $rowObj) { - $resultSetPrototype = $this->getResultSetPrototype(); - $resultSetPrototype->setArrayObjectPrototype($rowObj); - } - } - - /** - * Initialize features - * - * @param array $cfg Laminas configuration - * - * @return void - */ - public function initializeFeatures($cfg) - { - // Special case for PostgreSQL sequences: - if ($this->adapter->getDriver()->getDatabasePlatformName() == 'Postgresql') { - $maps = $cfg['vufind']['pgsql_seq_mapping'] ?? null; - if (isset($maps[$this->table])) { - if (!is_object($this->featureSet)) { - $this->featureSet = new Feature\FeatureSet(); - } - $this->featureSet->addFeature( - new Feature\SequenceFeature( - $maps[$this->table][0], - $maps[$this->table][1] - ) - ); - } - } - } - - /** - * Create a new row. - * - * @return object - */ - public function createRow() - { - $obj = clone $this->getResultSetPrototype()->getArrayObjectPrototype(); - - // If this is a PostgreSQL connection, we may need to initialize the ID - // from a sequence: - if ( - $this->adapter - && $this->adapter->getDriver()->getDatabasePlatformName() == 'Postgresql' - && $obj instanceof \VuFind\Db\Row\RowGateway - ) { - // Do we have a sequence feature? - $feature = $this->featureSet->getFeatureByClassName( - 'Laminas\Db\TableGateway\Feature\SequenceFeature' - ); - if ($feature) { - $key = $obj->getPrimaryKeyColumn(); - if (count($key) != 1) { - throw new \Exception('Unexpected number of key columns.'); - } - $col = $key[0]; - $obj->$col = $feature->nextSequenceId(); - } - } - - return $obj; - } - - /** - * Get access to another table. - * - * @param string $table Table name - * - * @return Gateway - */ - public function getDbTable($table) - { - return $this->tableManager->get($table); - } - - /** - * Begin a database transaction. - * - * @return void - * @throws Exception - */ - public function beginTransaction(): void - { - $this->getAdapter()->getDriver()->getConnection()->beginTransaction(); - } - - /** - * Commit a database transaction. - * - * @return void - * @throws Exception - */ - public function commitTransaction(): void - { - $this->getAdapter()->getDriver()->getConnection()->commit(); - } - - /** - * Roll back a database transaction. - * - * @return void - * @throws Exception - */ - public function rollBackTransaction(): void - { - $this->getAdapter()->getDriver()->getConnection()->rollback(); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/GatewayFactory.php b/module/VuFind/src/VuFind/Db/Table/GatewayFactory.php deleted file mode 100644 index 23554460be0..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/GatewayFactory.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Table; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * Generic table gateway factory. - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class GatewayFactory implements \Laminas\ServiceManager\Factory\FactoryInterface -{ - /** - * Return row prototype object (null if unavailable) - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * - * @return object - */ - protected function getRowPrototype(ContainerInterface $container, $requestedName) - { - $rowManager = $container->get(\VuFind\Db\Row\PluginManager::class); - // Map Table class to matching Row class. - $name = str_replace('\\Table\\', '\\Row\\', $requestedName); - return $rowManager->has($name) ? $rowManager->get($name) : null; - } - - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - $adapter = $container->get(\Laminas\Db\Adapter\Adapter::class); - $tm = $container->get(\VuFind\Db\Table\PluginManager::class); - $config = $container->get('config'); - $rowPrototype = $this->getRowPrototype($container, $requestedName); - $args = $options ? $options : []; - return new $requestedName( - $adapter, - $tm, - $config, - $rowPrototype, - ...$args - ); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/LoginToken.php b/module/VuFind/src/VuFind/Db/Table/LoginToken.php deleted file mode 100644 index 1650d4ea140..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/LoginToken.php +++ /dev/null @@ -1,197 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\ResultSet\ResultSetInterface; -use Laminas\Db\Sql\Expression; -use VuFind\Db\Row\LoginToken as LoginTokenRow; -use VuFind\Db\Row\RowGateway; -use VuFind\Exception\LoginToken as LoginTokenException; - -/** - * Table Definition for login_token - * - * @category VuFind - * @package Db_Table - * @author Jaro Ravila - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class LoginToken extends Gateway -{ - use ExpirationTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'login_token' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Check if a login token matches one in database. - * - * @param array $token array containing user id, token and series - * - * @return ?LoginTokenRow - * @throws LoginTokenException - */ - public function matchToken(array $token): ?LoginTokenRow - { - $userId = null; - foreach ($this->getBySeries($token['series']) as $row) { - $userId = $row->user_id; - if (hash_equals($row['token'], hash('sha256', $token['token']))) { - if (time() > $row['expires']) { - $row->delete(); - return null; - } - return $row; - } - } - if ($userId) { - throw new LoginTokenException('Tokens do not match', $userId); - } - return null; - } - - /** - * Delete all tokens in a given series - * - * @param string $series series - * @param ?int $currentTokenId Current token ID to keep - * - * @return void - */ - public function deleteBySeries(string $series, ?int $currentTokenId = null): void - { - $callback = function ($select) use ($series, $currentTokenId) { - $select->where->equalTo('series', $series); - if ($currentTokenId) { - $select->where->notEqualTo('id', $currentTokenId); - } - }; - $this->delete($callback); - } - - /** - * Delete all tokens for a user - * - * @param int $userId user identifier - * - * @return void - */ - public function deleteByUserId(int $userId): void - { - $this->delete(['user_id' => $userId]); - } - - /** - * Get tokens for a given user - * - * @param int $userId User identifier - * @param bool $grouped Whether to return results grouped by series - * - * @return array - */ - public function getByUserId(int $userId, bool $grouped = true): array - { - $callback = function ($select) use ($userId, $grouped) { - $select->where->equalTo('user_id', $userId); - $select->order('last_login DESC'); - if ($grouped) { - $select->columns( - [ - // RowGateway requires an id field: - 'id' => new Expression( - '1', - [], - [Expression::TYPE_IDENTIFIER] - ), - 'series', - 'user_id', - 'last_login' => new Expression( - 'MAX(?)', - ['last_login'], - [Expression::TYPE_IDENTIFIER] - ), - 'browser', - 'platform', - 'expires', - ] - ); - $select->group(['series', 'user_id', 'browser', 'platform', 'expires']); - } - }; - return iterator_to_array($this->select($callback)); - } - - /** - * Get token by series - * - * @param string $series Series identifier - * - * @return ResultSetInterface - */ - public function getBySeries(string $series): ResultSetInterface - { - return $this->select(compact('series')); - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function expirationCallback($select, $dateLimit) - { - // Date limit ignored since login token already contains an expiration time. - $select->where->lessThan('expires', time()); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/OaiResumption.php b/module/VuFind/src/VuFind/Db/Table/OaiResumption.php deleted file mode 100644 index 4afa64d0276..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/OaiResumption.php +++ /dev/null @@ -1,151 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Entity\OaiResumptionEntityInterface; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; - -/** - * Table Definition for oai_resumption - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class OaiResumption extends Gateway implements DbServiceAwareInterface -{ - use \VuFind\Db\Service\DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'oai_resumption' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Remove all expired tokens from the database. - * - * @return void - */ - public function removeExpired() - { - $callback = function ($select) { - $now = date('Y-m-d H:i:s'); - $select->where->lessThanOrEqualTo('expires', $now); - }; - $this->delete($callback); - } - - /** - * Retrieve a row from the database based on primary key; return null if it - * is not found. - * - * @param string $token The resumption token to retrieve. - * - * @return ?\VuFind\Db\Row\OaiResumption - * @deprecated Use OaiResumption::findWithId - */ - public function findToken($token) - { - return $this->findWithId($token); - } - - /** - * Retrieve a row from the database based on primary key; return null if it - * is not found. - * - * @param string $id Id used for the search. - * - * @return ?\VuFind\Db\Row\OaiResumption - */ - public function findWithId(string $id): ?OaiResumptionEntityInterface - { - return $this->select(['id' => $id])->current(); - } - - /** - * Retrieve a row from the database based on primary key and where the token is null. - * - * @param int $id Id used for the search. - * - * @return ?\VuFind\Db\Row\OaiResumption - * @todo In future, we should migrate data to prevent null token fields, which will make this method obsolete. - */ - final public function findWithLegacyIdToken(int $id): ?OaiResumptionEntityInterface - { - return $this->select(['id' => $id, 'token' => null])->current(); - } - - /** - * Retrieve a row from the database based on token; return null if it - * is not found. - * - * @param string $token Token used for the search. - * - * @return ?OaiResumptionEntityInterface - */ - public function findWithToken(string $token): ?OaiResumptionEntityInterface - { - return $this->select(['token' => $token])->current(); - } - - /** - * Create a new resumption token - * - * @param array $params Parameters associated with the token. - * @param int $expire Expiration time for token (Unix timestamp). - * - * @return int ID of new token - * - * @deprecated Use \VuFind\Db\Service\OaiResumptionService::createAndPersistToken() - */ - public function saveToken($params, $expire) - { - return $this->getDbService(\VuFind\Db\Service\OaiResumptionServiceInterface::class) - ->createAndPersistToken($params, $expire)->getId(); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/PluginManager.php b/module/VuFind/src/VuFind/Db/Table/PluginManager.php deleted file mode 100644 index 73412ae0d1a..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/PluginManager.php +++ /dev/null @@ -1,126 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ - -namespace VuFind\Db\Table; - -/** - * Database table plugin manager - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki - */ -class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager -{ - /** - * Default plugin aliases. - * - * @var array - */ - protected $aliases = [ - 'accesstoken' => AccessToken::class, - 'authhash' => AuthHash::class, - 'changetracker' => ChangeTracker::class, - 'comments' => Comments::class, - 'externalsession' => ExternalSession::class, - 'feedback' => Feedback::class, - 'logintoken' => LoginToken::class, - 'oairesumption' => OaiResumption::class, - 'ratings' => Ratings::class, - 'record' => Record::class, - 'resource' => Resource::class, - 'resourcetags' => ResourceTags::class, - 'search' => Search::class, - 'session' => Session::class, - 'shortlinks' => Shortlinks::class, - 'tags' => Tags::class, - 'user' => User::class, - 'usercard' => UserCard::class, - 'userlist' => UserList::class, - 'userresource' => UserResource::class, - ]; - - /** - * Default plugin factories. - * - * @var array - */ - protected $factories = [ - AccessToken::class => GatewayFactory::class, - AuthHash::class => GatewayFactory::class, - ChangeTracker::class => GatewayFactory::class, - Comments::class => GatewayFactory::class, - ExternalSession::class => GatewayFactory::class, - Feedback::class => GatewayFactory::class, - LoginToken::class => GatewayFactory::class, - OaiResumption::class => GatewayFactory::class, - Ratings::class => GatewayFactory::class, - Record::class => GatewayFactory::class, - Resource::class => ResourceFactory::class, - ResourceTags::class => CaseSensitiveTagsFactory::class, - Search::class => GatewayFactory::class, - Session::class => GatewayFactory::class, - Shortlinks::class => GatewayFactory::class, - Tags::class => CaseSensitiveTagsFactory::class, - User::class => UserFactory::class, - UserCard::class => GatewayFactory::class, - UserList::class => UserListFactory::class, - UserResource::class => GatewayFactory::class, - ]; - - /** - * Constructor - * - * Make sure plugins are properly initialized. - * - * @param mixed $configOrContainerInstance Configuration or container instance - * @param array $v3config If $configOrContainerInstance is a - * container, this value will be passed to the parent constructor. - */ - public function __construct( - $configOrContainerInstance = null, - array $v3config = [] - ) { - $this->addAbstractFactory(PluginFactory::class); - parent::__construct($configOrContainerInstance, $v3config); - } - - /** - * Return the name of the base class or interface that plug-ins must conform - * to. - * - * @return string - */ - protected function getExpectedInterface() - { - return Gateway::class; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Ratings.php b/module/VuFind/src/VuFind/Db/Table/Ratings.php deleted file mode 100644 index 1e40e396695..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Ratings.php +++ /dev/null @@ -1,283 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; - -/** - * Table Definition for ratings - * - * @category VuFind - * @package Db_Table - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Ratings extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'ratings' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get average rating and rating count associated with the specified resource. - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * @param ?int $userId User ID, or null for all users - * - * @return array Array with keys count and rating (between 0 and 100) - */ - public function getForResource(string $id, string $source, ?int $userId): array - { - $resourceService = $this->getDbService(ResourceServiceInterface::class); - $resource = $resourceService->getResourceByRecordId($id, $source); - if (!$resource) { - return [ - 'count' => 0, - 'rating' => 0, - ]; - } - - $callback = function ($select) use ($resource, $userId) { - $select->columns( - [ - // RowGateway requires an id field: - 'id' => new Expression( - '1', - [], - [Expression::TYPE_IDENTIFIER] - ), - 'count' => new Expression( - 'COUNT(?)', - [Select::SQL_STAR], - [Expression::TYPE_IDENTIFIER] - ), - 'rating' => new Expression( - 'FLOOR(AVG(?))', - ['rating'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->where->equalTo('ratings.resource_id', $resource->id); - if (null !== $userId) { - $select->where->equalTo('ratings.user_id', $userId); - } - }; - - $result = $this->select($callback)->current(); - return [ - 'count' => $result->count ?? 0, - 'rating' => $result->rating ?? 0, - ]; - } - - /** - * Get rating breakdown for the specified resource. - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * @param array $groups Group definition (key => [min, max]) - * - * @return array Array with keys count and rating (between 0 and 100) as well as - * an groups array with ratings from lowest to highest - */ - public function getCountsForResource( - string $id, - string $source, - array $groups - ): array { - $result = [ - 'count' => 0, - 'rating' => 0, - 'groups' => [], - ]; - foreach (array_keys($groups) as $key) { - $result['groups'][$key] = 0; - } - - $resourceService = $this->getDbService(ResourceServiceInterface::class); - $resource = $resourceService->getResourceByRecordId($id, $source); - if (!$resource) { - return $result; - } - - $callback = function ($select) use ($resource) { - $select->columns( - [ - // RowGateway requires an id field: - 'id' => new Expression( - '1', - [], - [Expression::TYPE_IDENTIFIER] - ), - 'count' => new Expression( - 'COUNT(?)', - [Select::SQL_STAR], - [Expression::TYPE_IDENTIFIER] - ), - 'rating' => 'rating', - ] - ); - $select->where->equalTo('ratings.resource_id', $resource->id); - $select->group('rating'); - }; - - $ratingTotal = 0; - $groupCount = 0; - foreach ($this->select($callback) as $rating) { - $result['count'] += $rating->count; - $ratingTotal += $rating->rating; - ++$groupCount; - if ($groups) { - foreach ($groups as $key => $range) { - if ( - $rating->rating >= $range[0] && $rating->rating <= $range[1] - ) { - $result['groups'][$key] = ($result['groups'][$key] ?? 0) - + $rating->count; - } - } - } - } - $result['rating'] = $groupCount ? floor($ratingTotal / $groupCount) : 0; - return $result; - } - - /** - * Deletes all ratings by a user. - * - * @param \VuFind\Db\Row\User $user User object - * - * @return void - */ - public function deleteByUser(\VuFind\Db\Row\User $user): void - { - $this->delete(['user_id' => $user->id]); - } - - /** - * Get statistics on use of ratings. - * - * @return array - */ - public function getStatistics(): array - { - $select = $this->sql->select(); - $select->columns( - [ - 'users' => new Expression( - 'COUNT(DISTINCT(?))', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'resources' => new Expression( - 'COUNT(DISTINCT(?))', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'total' => new Expression('COUNT(*)'), - ] - ); - $statement = $this->sql->prepareStatementForSqlObject($select); - $result = $statement->execute(); - return (array)$result->current(); - } - - /** - * Get a paginated result of all ratings made by the user. - * - * @param int $userId User ID - * @param int $limit Limit - * @param int $page Page - * @param string $sort Sort - * - * @return \Laminas\Paginator\Paginator - */ - public function getRatingsPaginator( - int $userId, - int $limit, - int $page, - string $sort - ): \Laminas\Paginator\Paginator { - $ratingSelect = new Select(); - $ratingSelect->from('ratings') - ->where->equalTo('ratings.user_id', $userId); - $ratingSelect->columns([ - 'resource_id', - 'user_id', - 'created', - 'id', - 'rating', - ]) - ->join( - ['re' => 'resource'], - 'ratings.resource_id = re.id', - ['record_id', 'source'], - Select::JOIN_LEFT - ); - $order = $sort ? $sort : 'created DESC'; - $ratingSelect->order($order); - if ($page > 0) { - $ratingSelect->offset($page); - } - $ratingSelect->limit($limit); - - $adapter = new \Laminas\Paginator\Adapter\LaminasDb\DbSelect($ratingSelect, $this->getSql()); - $paginator = new \Laminas\Paginator\Paginator($adapter); - $paginator->setItemCountPerPage($limit); - $paginator->setCurrentPageNumber($page); - return $paginator; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Record.php b/module/VuFind/src/VuFind/Db/Table/Record.php deleted file mode 100644 index 0e2673edab0..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Record.php +++ /dev/null @@ -1,166 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Predicate\Expression; -use Laminas\Db\Sql\Where; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\RecordServiceInterface; - -use function count; - -/** - * Table Definition for record - * - * @category VuFind - * @package Db_Table - * @author Markus Beh - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Record extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'record' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Find a record by id - * - * @param string $id Record ID - * @param string $source Record source - * - * @throws \Exception - * @return ?\VuFind\Db\Row\Record - */ - public function findRecord($id, $source) - { - return $this->select(['record_id' => $id, 'source' => $source])->current(); - } - - /** - * Find records by ids - * - * @param array $ids Record IDs - * @param string $source Record source - * - * @throws \Exception - * @return array Array of record row objects found - */ - public function findRecords($ids, $source) - { - if (empty($ids)) { - return []; - } - - $where = new Where(); - foreach ($ids as $id) { - $nested = $where->or->nest(); - $nested->addPredicates( - ['record_id' => $id, 'source' => $source] - ); - } - - return iterator_to_array($this->select($where)); - } - - /** - * Update an existing entry in the record table or create a new one - * - * @param string $id Record ID - * @param string $source Data source - * @param mixed $rawData Raw data from source (must be serializable) - * - * @return \VuFind\Db\Row\Record Updated or newly added record - * - * @deprecated Use RecordServiceInterface::updateRecord() - */ - public function updateRecord($id, $source, $rawData) - { - return $this->getDbService(RecordServiceInterface::class)->updateRecord($id, $source, $rawData); - } - - /** - * Clean up orphaned entries (i.e. entries that are not in favorites anymore) - * - * @return int Number of records deleted - */ - public function cleanup() - { - $callback = function ($select) { - $select->columns(['id']); - $select->join( - 'resource', - new Expression( - 'record.record_id = resource.record_id' - . ' AND record.source = resource.source' - ), - [] - )->join( - 'user_resource', - 'resource.id = user_resource.resource_id', - [], - $select::JOIN_LEFT - ); - $select->where->isNull('user_resource.id'); - }; - - $results = $this->select($callback); - foreach ($results as $result) { - $this->delete(['id' => $result['id']]); - } - - return count($results); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Resource.php b/module/VuFind/src/VuFind/Db/Table/Resource.php deleted file mode 100644 index 4827df775e7..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Resource.php +++ /dev/null @@ -1,320 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Date\Converter as DateConverter; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceServiceInterface; - -use function in_array; - -/** - * Table Definition for resource - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Resource extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Loader for record populator - * - * @var callable - */ - protected $resourcePopulatorLoader; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param DateConverter $dateConverter Date converter - * @param callable $resourcePopulatorLoader Resource populator loader - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - array $cfg, - ?RowGateway $rowObj, - protected DateConverter $dateConverter, - callable $resourcePopulatorLoader, - string $table = 'resource' - ) { - $this->resourcePopulatorLoader = $resourcePopulatorLoader; - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Look up a row for the specified resource. - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * @param bool $create If true, create the row if it - * does not yet exist. - * @param \VuFind\RecordDriver\AbstractBase $driver A record driver for the - * resource being created (optional -- improves efficiency if provided, but will - * be auto-loaded as needed if left null). - * - * @return \VuFind\Db\Row\Resource|null Matching row if found or created, null - * otherwise. - * - * @deprecated Use ResourceServiceInterface::getResourceByRecordId() or - * \VuFind\Record\ResourcePopulator::getOrCreateResourceForDriver() or - * \VuFind\Record\ResourcePopulator::getOrCreateResourceForRecordId() as appropriate. - */ - public function findResource( - $id, - $source = DEFAULT_SEARCH_BACKEND, - $create = true, - $driver = null - ) { - if (empty($id)) { - throw new \Exception('Resource ID cannot be empty'); - } - $select = $this->select(['record_id' => $id, 'source' => $source]); - $result = $select->current(); - - // Create row if it does not already exist and creation is enabled: - if (empty($result) && $create) { - $resourcePopulator = ($this->resourcePopulatorLoader)(); - $result = $driver - ? $resourcePopulator->createAndPersistResourceForDriver($driver) - : $resourcePopulator->createAndPersistResourceForRecordId($id, $source); - } - return $result; - } - - /** - * Look up a rowset for a set of specified resources. - * - * @param array $ids Array of IDs - * @param string $source Source of records to look up - * - * @return ResourceEntityInterface[] - * - * @deprecated Use ResourceServiceInterface::getResourcesByRecordIds() - */ - public function findResources($ids, $source = DEFAULT_SEARCH_BACKEND) - { - return $this->getDbService(ResourceServiceInterface::class)->getResourcesByRecordIds($ids, $source); - } - - /** - * Get a set of records from the requested favorite list. - * - * @param string $user ID of user owning favorite list - * @param string $list ID of list to retrieve (null for all favorites) - * @param array $tags Tags to use for limiting results - * @param string $sort Resource table field to use for sorting (null for no particular sort). - * @param int $offset Offset for results - * @param int $limit Limit for results (null for none) - * @param ?bool $caseSensitiveTags Should tags be searched case sensitively (null for configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getFavorites( - $user, - $list = null, - $tags = [], - $sort = null, - $offset = 0, - $limit = null, - $caseSensitiveTags = null - ) { - // Set up base query: - return $this->select( - function ($s) use ($user, $list, $tags, $sort, $offset, $limit, $caseSensitiveTags) { - $columns = [Select::SQL_STAR]; - $s->columns($columns); - $s->join( - 'user_resource', - 'resource.id = user_resource.resource_id', - ['last_saved' => new Expression('MAX(saved)')] - ); - $s->where->equalTo('user_resource.user_id', $user); - // Adjust for list if necessary: - if (null !== $list) { - $s->where->equalTo('user_resource.list_id', $list); - } - // Adjust for tags if necessary: - if (!empty($tags)) { - $linkingTable = $this->getDbTable('ResourceTags'); - foreach ($tags as $tag) { - $matches = $linkingTable->getResourcesForTag($tag, $user, $list, $caseSensitiveTags)->toArray(); - $getId = function ($i) { - return $i['resource_id']; - }; - $s->where->in('resource_id', array_map($getId, $matches)); - } - } - if ($offset > 0) { - $s->offset($offset); - } - if (null !== $limit) { - $s->limit($limit); - } - - $s->group(['resource.id']); - - // Apply sorting, if necessary: - if ($sort == 'last_saved' || $sort == 'last_saved DESC') { - $s->order($sort); - } elseif (!empty($sort)) { - Resource::applySort($s, $sort, 'resource', $columns); - } - } - ); - } - - /** - * Get a set of records that do not have metadata stored in the resource - * table. - * - * @return ResourceEntityInterface[] - * - * @deprecated Use ResourceServiceInterface::findMissingMetadata() - */ - public function findMissingMetadata() - { - return $this->getDbService(ResourceServiceInterface::class)->findMissingMetadata(); - } - - /** - * Update the database to reflect a changed record identifier. - * - * @param string $oldId Original record ID - * @param string $newId Revised record ID - * @param string $source Record source - * - * @return void - * - * @deprecated Use \VuFind\Record\RecordIdUpdater::updateRecordId() - */ - public function updateRecordId($oldId, $newId, $source = DEFAULT_SEARCH_BACKEND) - { - $resourceService = $this->getDbService(ResourceServiceInterface::class); - if ( - $oldId !== $newId - && $resource = $resourceService->getResourceByRecordId($oldId, $source) - ) { - $tableObjects = []; - // Do this as a transaction to prevent odd behavior: - $connection = $this->getAdapter()->getDriver()->getConnection(); - $connection->beginTransaction(); - // Does the new ID already exist? - if ($newResource = $resourceService->getResourceByRecordId($newId, $source)) { - // Special case: merge new ID and old ID: - foreach (['comments', 'userresource', 'resourcetags'] as $table) { - $tableObjects[$table] = $this->getDbTable($table); - $tableObjects[$table]->update( - ['resource_id' => $newResource->id], - ['resource_id' => $resource->id] - ); - } - $resource->delete(); - } else { - // Default case: just update the record ID: - $resource->record_id = $newId; - $resource->save(); - } - // Done -- commit the transaction: - $connection->commit(); - - // Deduplicate rows where necessary (this can be safely done outside - // of the transaction): - if (isset($tableObjects['resourcetags'])) { - $tableObjects['resourcetags']->deduplicate(); - } - if (isset($tableObjects['userresource'])) { - $tableObjects['userresource']->deduplicate(); - } - } - } - - /** - * Apply a sort parameter to a query on the resource table. - * - * @param \Laminas\Db\Sql\Select $query Query to modify - * @param string $sort Field to use for sorting (may include - * 'desc' qualifier) - * @param string $alias Alias to the resource table (defaults to - * 'resource') - * @param array $columns Existing list of columns to select - * - * @return void - */ - public static function applySort($query, $sort, $alias = 'resource', $columns = []) - { - // Apply sorting, if necessary: - $legalSorts = [ - 'title', 'title desc', 'author', 'author desc', 'year', 'year desc', - ]; - if (!empty($sort) && in_array(strtolower($sort), $legalSorts)) { - // Strip off 'desc' to obtain the raw field name -- we'll need it - // to sort null values to the bottom: - $parts = explode(' ', $sort); - $rawField = trim($parts[0]); - - // Start building the list of sort fields: - $order = []; - - // The title field can't be null, so don't bother with the extra - // isnull() sort in that case. - if (strtolower($rawField) != 'title') { - $expression = new Expression( - 'case when ? is null then 1 else 0 end', - [$alias . '.' . $rawField], - [Expression::TYPE_IDENTIFIER] - ); - $query->columns(array_merge($columns, [$expression])); - $order[] = $expression; - } - - // Apply the user-specified sort: - $order[] = $alias . '.' . $sort; - - // Inject the sort preferences into the query object: - $query->order($order); - } - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/ResourceFactory.php b/module/VuFind/src/VuFind/Db/Table/ResourceFactory.php deleted file mode 100644 index 52c08d3cddd..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/ResourceFactory.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Table; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * Resource table gateway factory. - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class ResourceFactory extends GatewayFactory -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - $converter = $container->get(\VuFind\Date\Converter::class); - // Wrapper needed to avoid circular dependency: - $populatorLoader = function () use ($container) { - return $container->get(\VuFind\Record\ResourcePopulator::class); - }; - return parent::__invoke($container, $requestedName, [$converter, $populatorLoader]); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/ResourceTags.php b/module/VuFind/src/VuFind/Db/Table/ResourceTags.php deleted file mode 100644 index 85823aaebbf..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/ResourceTags.php +++ /dev/null @@ -1,831 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use DateTime; -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceTagsServiceInterface; - -use function count; -use function in_array; -use function is_array; - -/** - * Table Definition for resource_tags - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class ResourceTags extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param RowGateway $rowObj Row prototype object (null for default) - * @param bool $caseSensitive Are tags case sensitive? - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - protected $caseSensitive = false, - $table = 'resource_tags' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Look up a row for the specified resource. - * - * @param string $resource ID of resource to link up - * @param string $tag ID of tag to link up - * @param string $user ID of user creating link (optional but recommended) - * @param string $list ID of list to link up (optional) - * @param string $posted Posted date (optional -- omit for current) - * - * @return void - * - * @deprecated Use ResourceTagsServiceInterface::createLink() - */ - public function createLink( - $resource, - $tag, - $user = null, - $list = null, - $posted = null - ) { - $this->getDbService(ResourceTagsServiceInterface::class)->createLink( - $resource, - $tag, - $user, - $list, - $posted ? DateTime::createFromFormat('Y-m-d H:i:s', $posted) : null - ); - } - - /** - * Check whether or not the specified tags are present in the table. - * - * @param array $ids IDs to check. - * - * @return array Associative array with two keys: present and missing - * - * @deprecated - */ - public function checkForTags($ids) - { - // Set up return arrays: - $retVal = ['present' => [], 'missing' => []]; - - // Look up IDs in the table: - $callback = function ($select) use ($ids) { - $select->where->in('tag_id', $ids); - }; - $results = $this->select($callback); - - // Record all IDs that are present: - foreach ($results as $current) { - $retVal['present'][] = $current->tag_id; - } - $retVal['present'] = array_unique($retVal['present']); - - // Detect missing IDs: - foreach ($ids as $current) { - if (!in_array($current, $retVal['present'])) { - $retVal['missing'][] = $current; - } - } - - // Send back the results: - return $retVal; - } - - /** - * Get resources associated with a particular tag. - * - * @param string $tag Tag to match - * @param string $userId ID of user owning favorite list - * @param string $listId ID of list to retrieve (null for all favorites) - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getResourcesForTag($tag, $userId, $listId = null, $caseSensitive = null) - { - $callback = function ($select) use ($tag, $userId, $listId, $caseSensitive) { - $select->columns( - [ - 'resource_id' => new Expression( - 'DISTINCT(?)', - ['resource_tags.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), Select::SQL_STAR, - ] - ); - $select->join( - ['t' => 'tags'], - 'resource_tags.tag_id = t.id', - [] - ); - if ($caseSensitive ?? $this->caseSensitive) { - $select->where->equalTo('t.tag', $tag); - } else { - $select->where->literal('lower(t.tag) = lower(?)', [$tag]); - } - $select->where->equalTo('resource_tags.user_id', $userId); - if (null !== $listId) { - $select->where->equalTo('resource_tags.list_id', $listId); - } - }; - - return $this->select($callback); - } - - /** - * Get lists associated with a particular tag. - * - * @param string|array|null $tag Tag to match (null for all) - * @param string|array|null $listId List ID to retrieve (null for all) - * @param bool $publicOnly Whether to return only public lists - * @param bool $andTags Use AND operator when filtering by tag. - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getListsForTag( - $tag, - $listId = null, - $publicOnly = true, - $andTags = true, - $caseSensitive = null - ) { - $tag = (array)($tag ?? []); - $listId = $listId ? (array)$listId : null; - - $callback = function ($select) use ( - $tag, - $listId, - $publicOnly, - $andTags, - $caseSensitive - ) { - $select->columns( - ['id' => new Expression('min(resource_tags.id)'), 'list_id'] - ); - - $select->join( - ['t' => 'tags'], - 'resource_tags.tag_id = t.id', - [] - ); - $select->join( - ['l' => 'user_list'], - 'resource_tags.list_id = l.id', - [] - ); - - // Discard tags assigned to a user resource. - $select->where->isNull('resource_id'); - - // Restrict to tags by list owner - $select->where->and->equalTo( - 'resource_tags.user_id', - new Expression('l.user_id') - ); - - if ($listId) { - $select->where->and->in('resource_tags.list_id', $listId); - } - if ($publicOnly) { - $select->where->and->equalTo('public', 1); - } - if ($tag) { - if ($caseSensitive ?? $this->caseSensitive) { - $select->where->and->in('t.tag', $tag); - } else { - $lowerTags = array_map( - function ($t) { - return new Expression( - 'lower(?)', - [$t], - [Expression::TYPE_VALUE] - ); - }, - $tag - ); - $select->where->and->in( - new Expression('lower(t.tag)'), - $lowerTags - ); - } - } - $select->group('resource_tags.list_id'); - - if ($tag && $andTags) { - // Use AND operator for tags - $select->having->literal( - 'count(distinct(resource_tags.tag_id)) = ?', - count(array_unique($tag)) - ); - } - $select->order('resource_tags.list_id'); - }; - - return $this->select($callback); - } - - /** - * Get statistics on use of tags. - * - * @param bool $extended Include extended (unique/anonymous) stats. - * @param ?bool $caseSensitiveTags Should we treat tags as case-sensitive? (null for configured behavior) - * - * @return array - */ - public function getStatistics($extended = false, $caseSensitiveTags = null) - { - $select = $this->sql->select(); - $select->columns( - [ - 'users' => new Expression( - 'COUNT(DISTINCT(?))', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'resources' => new Expression( - 'COUNT(DISTINCT(?))', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'total' => new Expression('COUNT(*)'), - ] - ); - $statement = $this->sql->prepareStatementForSqlObject($select); - $result = $statement->execute(); - $stats = (array)$result->current(); - if ($extended) { - $stats['unique'] = count($this->getUniqueTags(caseSensitive: $caseSensitiveTags)); - $stats['anonymous'] = $this->getAnonymousCount(); - } - return $stats; - } - - /** - * Unlink rows for the specified resource. - * - * @param string|array $resource ID (or array of IDs) of resource(s) to - * unlink (null for ALL matching resources) - * @param string $user ID of user removing links - * @param string $list ID of list to unlink (null for ALL matching - * tags, 'none' for tags not in a list, true for tags only found in a list) - * @param string|array $tag ID or array of IDs of tag(s) to unlink (null - * for ALL matching tags) - * - * @return void - * - * @deprecated Use ResourceTagsServiceInterface::destroyResourceTagsLinksForUser() or - * ResourceTagsServiceInterface::destroyNonListResourceTagsLinksForUser() or - * ResourceTagsServiceInterface::destroyAllListResourceTagsLinksForUser() - */ - public function destroyResourceLinks($resource, $user, $list = null, $tag = null) - { - $callback = function ($select) use ($resource, $user, $list, $tag) { - $select->where->equalTo('user_id', $user); - if (null !== $resource) { - $select->where->in('resource_id', (array)$resource); - } - if (null !== $list) { - if (true === $list) { - // special case -- if $list is set to boolean true, we - // want to only delete tags that are associated with lists. - $select->where->isNotNull('list_id'); - } elseif ('none' === $list) { - // special case -- if $list is set to the string "none", we - // want to delete tags that are not associated with lists. - $select->where->isNull('list_id'); - } else { - $select->where->equalTo('list_id', $list); - } - } - if (null !== $tag) { - if (is_array($tag)) { - $select->where->in('tag_id', $tag); - } else { - $select->where->equalTo('tag_id', $tag); - } - } - }; - $this->processDestroyLinks($callback); - } - - /** - * Unlink rows for the specified user list. - * - * @param string $list ID of list to unlink - * @param string $user ID of user removing links - * @param string|array $tag ID or array of IDs of tag(s) to unlink (null - * for ALL matching tags) - * - * @return void - * - * @deprecated Use ResourceTagsServiceInterface::destroyUserListLinks() - */ - public function destroyListLinks($list, $user, $tag = null) - { - $callback = function ($select) use ($user, $list, $tag) { - $select->where->equalTo('user_id', $user); - // retrieve tags assigned to a user list - // and filter out user resource tags - // (resource_id is NULL for list tags). - $select->where->isNull('resource_id'); - $select->where->equalTo('list_id', $list); - - if (null !== $tag) { - if (is_array($tag)) { - $select->where->in('tag_id', $tag); - } else { - $select->where->equalTo('tag_id', $tag); - } - } - }; - $this->processDestroyLinks($callback); - } - - /** - * Process link rows marked to be destroyed. - * - * @param Object $callback Callback function for selecting deleted rows. - * - * @return void - * - * @deprecated - */ - protected function processDestroyLinks($callback) - { - // Get a list of all tag IDs being deleted; we'll use these for - // orphan-checking: - $potentialOrphans = $this->select($callback); - - // Now delete the unwanted rows: - $this->delete($callback); - - // Check for orphans: - if (count($potentialOrphans) > 0) { - $ids = []; - foreach ($potentialOrphans as $current) { - $ids[] = $current->tag_id; - } - $checkResults = $this->checkForTags(array_unique($ids)); - if (count($checkResults['missing']) > 0) { - $tagTable = $this->getDbTable('Tags'); - $tagTable->deleteByIdArray($checkResults['missing']); - } - } - } - - /** - * Get count of anonymous tags - * - * @return int count - */ - public function getAnonymousCount() - { - $callback = function ($select) { - $select->where->isNull('user_id'); - }; - return count($this->select($callback)); - } - - /** - * Assign anonymous tags to the specified user ID. - * - * @param int $id User ID to own anonymous tags. - * - * @return void - */ - public function assignAnonymousTags($id) - { - $callback = function ($select) { - $select->where->isNull('user_id'); - }; - $this->update(['user_id' => $id], $callback); - } - - /** - * Gets unique resources from the table - * - * @param string $userId ID of user - * @param string $resourceId ID of the resource - * @param string $tagId ID of the tag - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getUniqueResources( - $userId = null, - $resourceId = null, - $tagId = null - ) { - $callback = function ($select) use ($userId, $resourceId, $tagId) { - $select->columns( - [ - 'resource_id' => new Expression( - 'MAX(?)', - ['resource_tags.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag_id' => new Expression( - 'MAX(?)', - ['resource_tags.tag_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'list_id' => new Expression( - 'MAX(?)', - ['resource_tags.list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'user_id' => new Expression( - 'MAX(?)', - ['resource_tags.user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MAX(?)', - ['resource_tags.id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['r' => 'resource'], - 'resource_tags.resource_id = r.id', - ['title' => 'title'] - ); - if (null !== $userId) { - $select->where->equalTo('resource_tags.user_id', $userId); - } - if (null !== $resourceId) { - $select->where->equalTo('resource_tags.resource_id', $resourceId); - } - if (null !== $tagId) { - $select->where->equalTo('resource_tags.tag_id', $tagId); - } - $select->group(['resource_id', 'title']); - $select->order(['title']); - }; - return $this->select($callback); - } - - /** - * Gets unique tags from the table - * - * @param string $userId ID of user - * @param string $resourceId ID of the resource - * @param string $tagId ID of the tag - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getUniqueTags($userId = null, $resourceId = null, $tagId = null, $caseSensitive = null) - { - $callback = function ($select) use ($userId, $resourceId, $tagId, $caseSensitive) { - $select->columns( - [ - 'resource_id' => new Expression( - 'MAX(?)', - ['resource_tags.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag_id' => new Expression( - 'MAX(?)', - ['resource_tags.tag_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'list_id' => new Expression( - 'MAX(?)', - ['resource_tags.list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'user_id' => new Expression( - 'MAX(?)', - ['resource_tags.user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MAX(?)', - ['resource_tags.id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['t' => 'tags'], - 'resource_tags.tag_id = t.id', - [ - 'tag' => ($caseSensitive ?? $this->caseSensitive) ? 'tag' : new Expression('lower(tag)'), - ] - ); - if (null !== $userId) { - $select->where->equalTo('resource_tags.user_id', $userId); - } - if (null !== $resourceId) { - $select->where->equalTo('resource_tags.resource_id', $resourceId); - } - if (null !== $tagId) { - $select->where->equalTo('resource_tags.tag_id', $tagId); - } - $select->group(['tag_id', 'tag']); - $select->order([new Expression('lower(tag)'), 'tag']); - }; - return $this->select($callback); - } - - /** - * Gets unique users from the table - * - * @param string $userId ID of user - * @param string $resourceId ID of the resource - * @param string $tagId ID of the tag - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getUniqueUsers($userId = null, $resourceId = null, $tagId = null) - { - $callback = function ($select) use ($userId, $resourceId, $tagId) { - $select->columns( - [ - 'resource_id' => new Expression( - 'MAX(?)', - ['resource_tags.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag_id' => new Expression( - 'MAX(?)', - ['resource_tags.tag_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'list_id' => new Expression( - 'MAX(?)', - ['resource_tags.list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'user_id' => new Expression( - 'MAX(?)', - ['resource_tags.user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MAX(?)', - ['resource_tags.id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['u' => 'user'], - 'resource_tags.user_id = u.id', - ['username' => 'username'] - ); - if (null !== $userId) { - $select->where->equalTo('resource_tags.user_id', $userId); - } - if (null !== $resourceId) { - $select->where->equalTo('resource_tags.resource_id', $resourceId); - } - if (null !== $tagId) { - $select->where->equalTo('resource_tags.tag_id', $tagId); - } - $select->group(['user_id', 'username']); - $select->order(['username']); - }; - return $this->select($callback); - } - - /** - * Given an array for sorting database results, make sure the tag field is - * sorted in a case-insensitive fashion. - * - * @param array $order Order settings - * - * @return array - */ - protected function formatTagOrder($order) - { - if (empty($order)) { - return $order; - } - $newOrder = []; - foreach ((array)$order as $current) { - $newOrder[] = $current == 'tag' - ? new Expression('lower(tag)') : $current; - } - return $newOrder; - } - - /** - * Get Resource Tags - * - * @param string $userId ID of user - * @param string $resourceId ID of the resource - * @param string $tagId ID of the tag - * @param string $order The order in which to return the data - * @param string $page The page number to select - * @param string $limit The number of items to fetch - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Paginator\Paginator - */ - public function getResourceTags( - $userId = null, - $resourceId = null, - $tagId = null, - $order = null, - $page = null, - $limit = 20, - $caseSensitive = null - ) { - $order = (null !== $order) - ? [$order] - : ['username', 'tag', 'title']; - - $sql = $this->getSql(); - $select = $sql->select(); - $select->join( - ['t' => 'tags'], - 'resource_tags.tag_id = t.id', - [ - 'tag' => ($caseSensitive ?? $this->caseSensitive) ? 'tag' : new Expression('lower(tag)'), - ] - ); - $select->join( - ['u' => 'user'], - 'resource_tags.user_id = u.id', - ['username' => 'username'] - ); - $select->join( - ['r' => 'resource'], - 'resource_tags.resource_id = r.id', - ['title', 'record_id', 'source'] - ); - if (null !== $userId) { - $select->where->equalTo('resource_tags.user_id', $userId); - } - if (null !== $resourceId) { - $select->where->equalTo('resource_tags.resource_id', $resourceId); - } - if (null !== $tagId) { - $select->where->equalTo('resource_tags.tag_id', $tagId); - } - $select->order($this->formatTagOrder($order)); - - if (null !== $page) { - $select->limit($limit); - $select->offset($limit * ($page - 1)); - } - - $adapter = new \Laminas\Paginator\Adapter\LaminasDb\DbSelect($select, $sql); - $paginator = new \Laminas\Paginator\Paginator($adapter); - $paginator->setItemCountPerPage($limit); - if (null !== $page) { - $paginator->setCurrentPageNumber($page); - } - return $paginator; - } - - /** - * Delete a group of tags. - * - * @param array $ids IDs of tags to delete. - * - * @return int Count of $ids - */ - public function deleteByIdArray($ids) - { - // Do nothing if we have no IDs to delete! - if (empty($ids)) { - return; - } - - $callback = function ($select) use ($ids) { - $select->where->in('id', $ids); - }; - $this->delete($callback); - return count($ids); - } - - /** - * Get a list of duplicate rows (this sometimes happens after merging IDs, - * for example after a Summon resource ID changes). - * - * @return mixed - */ - public function getDuplicates() - { - $callback = function ($select) { - $select->columns( - [ - 'resource_id' => new Expression( - 'MIN(?)', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag_id' => new Expression( - 'MIN(?)', - ['tag_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'list_id' => new Expression( - 'MIN(?)', - ['list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'user_id' => new Expression( - 'MIN(?)', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'cnt' => new Expression( - 'COUNT(?)', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MIN(?)', - ['id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->group(['resource_id', 'tag_id', 'list_id', 'user_id']); - $select->having('COUNT(resource_id) > 1'); - }; - return $this->select($callback); - } - - /** - * Deduplicate rows (sometimes necessary after merging foreign key IDs). - * - * @return void - */ - public function deduplicate() - { - foreach ($this->getDuplicates() as $dupe) { - $callback = function ($select) use ($dupe) { - // match on all relevant IDs in duplicate group - $select->where( - [ - 'resource_id' => $dupe['resource_id'], - 'tag_id' => $dupe['tag_id'], - 'list_id' => $dupe['list_id'], - 'user_id' => $dupe['user_id'], - ] - ); - // getDuplicates returns the minimum id in the set, so we want to - // delete all of the duplicates with a higher id value. - $select->where->greaterThan('id', $dupe['id']); - }; - $this->delete($callback); - } - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Search.php b/module/VuFind/src/VuFind/Db/Table/Search.php deleted file mode 100644 index d52e3faa21d..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Search.php +++ /dev/null @@ -1,285 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Adapter\ParameterContainer; -use Laminas\Db\TableGateway\Feature; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\SearchServiceInterface; -use VuFind\Search\NormalizedSearch; -use VuFind\Search\SearchNormalizer; - -use function count; -use function is_object; - -/** - * Table Definition for search - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Search extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - use ExpirationTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'search' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Initialize features - * - * @param array $cfg Laminas configuration - * - * @return void - */ - public function initializeFeatures($cfg) - { - // Special case for PostgreSQL inserts -- we need to provide an extra - // clue so that the database knows how to write bytea data correctly: - if ($this->adapter->getDriver()->getDatabasePlatformName() == 'Postgresql') { - if (!is_object($this->featureSet)) { - $this->featureSet = new Feature\FeatureSet(); - } - $eventFeature = new Feature\EventFeature(); - $eventFeature->getEventManager()->attach( - Feature\EventFeature::EVENT_PRE_INITIALIZE, - [$this, 'onPreInit'] - ); - $this->featureSet->addFeature($eventFeature); - } - - parent::initializeFeatures($cfg); - } - - /** - * Customize the database object to include extra metadata about the - * search_object field so that it will be written correctly. This is - * triggered only when we're interacting with PostgreSQL; MySQL works fine - * without the extra hint. - * - * @param object $event Event object - * - * @return void - */ - public function onPreInit($event) - { - $driver = $event->getTarget()->getAdapter()->getDriver(); - $statement = $driver->createStatement(); - $params = new ParameterContainer(); - $params->offsetSetErrata('search_object', ParameterContainer::TYPE_LOB); - $statement->setParameterContainer($params); - $driver->registerStatementPrototype($statement); - } - - /** - * Destroy unsaved searches belonging to the specified session/user. - * - * @param string $sid Session ID of current user. - * @param int $uid User ID of current user (optional). - * - * @return void - * - * @deprecated Use SearchServiceInterface::destroySession() - */ - public function destroySession($sid, $uid = null) - { - $this->getDbService(SearchServiceInterface::class)->destroySession($sid, $uid); - } - - /** - * Get an array of rows for the specified user. - * - * @param string $sid Session ID of current user. - * @param int $uid User ID of current user (optional). - * - * @return array Matching SearchEntry objects. - * - * @deprecated Use SearchServiceInterface::getSearches() - */ - public function getSearches($sid, $uid = null) - { - return $this->getDbService(SearchServiceInterface::class)->getSearches($sid, $uid); - } - - /** - * Get a single row matching a primary key value. - * - * @param int $id Primary key value - * @param bool $exceptionIfMissing Should we throw an exception if the row is - * missing? - * - * @throws \Exception - * @return ?\VuFind\Db\Row\Search - * - * @deprecated - */ - public function getRowById($id, $exceptionIfMissing = true) - { - $row = $this->select(['id' => $id])->current(); - if (empty($row) && $exceptionIfMissing) { - throw new \Exception('Cannot find id ' . $id); - } - return $row; - } - - /** - * Get a single row, enforcing user ownership. Returns row if found, null - * otherwise. - * - * @param int $id Primary key value - * @param string $sessId Current user session ID - * @param int $userId Current logged-in user ID (or null if none) - * - * @return ?\VuFind\Db\Row\Search - * - * @deprecated Use SearchServiceInterface::getSearchByIdAndOwner() - */ - public function getOwnedRowById($id, $sessId, $userId) - { - return $this->getDbService(SearchServiceInterface::class)->getSearchByIdAndOwner($id, $sessId, $userId); - } - - /** - * Get scheduled searches. - * - * @return array Array of VuFind\Db\Row\Search objects. - * - * @deprecated Use SearchServiceInterface::getScheduledSearches() - */ - public function getScheduledSearches() - { - return $this->getDbService(SearchServiceInterface::class)->getScheduledSearches(); - } - - /** - * Return existing search table rows matching the provided normalized search. - * - * @param NormalizedSearch $normalized Normalized search to match against - * @param string $sessionId Current session ID - * @param int|null $userId Current user ID - * @param int $limit Max rows to retrieve - * (default = no limit) - * - * @return \VuFind\Db\Row\Search[] - * - * @deprecated Use SearchNormalizer::getSearchesMatchingNormalizedSearch() - */ - public function getSearchRowsMatchingNormalizedSearch( - NormalizedSearch $normalized, - string $sessionId, - ?int $userId, - int $limit = PHP_INT_MAX - ) { - // Fetch all rows with the same CRC32 and try to match with the URL - $checksum = $normalized->getChecksum(); - $callback = function ($select) use ($checksum, $sessionId, $userId) { - $nest = $select->where - ->equalTo('checksum', $checksum) - ->and - ->nest - ->equalTo('session_id', $sessionId)->and->equalTo('saved', 0); - if (!empty($userId)) { - $nest->or->equalTo('user_id', $userId); - } - }; - $results = []; - foreach ($this->select($callback) as $match) { - $minified = $match->getSearchObjectOrThrowException(); - if ($normalized->isEquivalentToMinifiedSearch($minified)) { - $results[] = $match; - if (count($results) >= $limit) { - break; - } - } - } - return $results; - } - - /** - * Add a search into the search table (history) - * - * @param SearchNormalizer $normalizer Search manager - * @param \VuFind\Search\Base\Results $results Search to save - * @param string $sessionId Current session ID - * @param int|null $userId Current user ID - * - * @return \VuFind\Db\Row\Search - * - * @deprecated Use SearchNormalizer::saveNormalizedSearch() - */ - public function saveSearch( - SearchNormalizer $normalizer, - $results, - $sessionId, - $userId - ) { - return $normalizer->saveNormalizedSearch($results, $sessionId, $userId); - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - protected function expirationCallback($select, $dateLimit) - { - $select->where->lessThan('created', $dateLimit)->equalTo('saved', 0); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Session.php b/module/VuFind/src/VuFind/Db/Table/Session.php deleted file mode 100644 index a1c830abcae..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Session.php +++ /dev/null @@ -1,178 +0,0 @@ - - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; -use VuFind\Exception\SessionExpired as SessionExpiredException; - -use function intval; - -/** - * Table Definition for session - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Session extends Gateway -{ - use ExpirationTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'session' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Retrieve an object from the database based on session ID; create a new - * row if no existing match is found. - * - * @param string $sid Session ID to retrieve - * @param bool $create Should we create rows that don't already exist? - * - * @return ?\VuFind\Db\Row\Session - */ - public function getBySessionId($sid, $create = true) - { - $row = $this->select(['session_id' => $sid])->current(); - if ($create && empty($row)) { - $row = $this->createRow(); - $row->session_id = $sid; - $row->created = date('Y-m-d H:i:s'); - } - return $row; - } - - /** - * Retrieve data for the given session ID. - * - * @param string $sid Session ID to retrieve - * @param int $lifetime Session lifetime (in seconds) - * - * @throws SessionExpiredException - * @return string Session data - */ - public function readSession($sid, $lifetime) - { - $s = $this->getBySessionId($sid); - - // enforce lifetime of this session data - if (!empty($s->last_used) && $s->last_used + $lifetime <= time()) { - throw new SessionExpiredException('Session expired!'); - } - - // if we got this far, session is good -- update last access time, save - // changes, and return data. - $s->last_used = time(); - $s->save(); - return empty($s->data) ? '' : $s->data; - } - - /** - * Store data for the given session ID. - * - * @param string $sid Session ID to retrieve - * @param string $data Data to store - * - * @return void - */ - public function writeSession($sid, $data) - { - $s = $this->getBySessionId($sid); - $s->last_used = time(); - $s->data = $data; - $s->save(); - } - - /** - * Destroy data for the given session ID. - * - * @param string $sid Session ID to erase - * - * @return void - */ - public function destroySession($sid) - { - $s = $this->getBySessionId($sid, false); - if (!empty($s)) { - $s->delete(); - } - } - - /** - * Garbage collect expired sessions. - * - * @param int $sess_maxlifetime Maximum session lifetime. - * - * @return int - */ - public function garbageCollect($sess_maxlifetime) - { - $callback = function ($select) use ($sess_maxlifetime) { - $select->where - ->lessThan('last_used', time() - intval($sess_maxlifetime)); - }; - return $this->delete($callback); - } - - /** - * Update the select statement to find records to delete. - * - * @param Select $select Select clause - * @param string $dateLimit Date threshold of an "expired" record in format - * 'Y-m-d H:i:s'. - * - * @return void - */ - protected function expirationCallback($select, $dateLimit) - { - $select->where->lessThan('last_used', strtotime($dateLimit)); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Shortlinks.php b/module/VuFind/src/VuFind/Db/Table/Shortlinks.php deleted file mode 100644 index 072f122119c..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Shortlinks.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; - -/** - * Table Definition for shortlinks - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Shortlinks extends Gateway -{ - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'shortlinks' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/Tags.php b/module/VuFind/src/VuFind/Db/Table/Tags.php deleted file mode 100644 index 584c362cefc..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Tags.php +++ /dev/null @@ -1,646 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Predicate\Predicate; -use Laminas\Db\Sql\Select; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceTagsServiceInterface; -use VuFind\Db\Service\TagServiceInterface; - -use function count; -use function is_callable; - -/** - * Table Definition for tags - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class Tags extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param RowGateway $rowObj Row prototype object (null for default) - * @param bool $caseSensitive Are tags case sensitive? - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - protected $caseSensitive = false, - $table = 'tags' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get the row associated with a specific tag string. - * - * @param string $tag Tag to look up. - * @param bool $create Should we create the row if it does not exist? - * @param bool $firstOnly Should we return the first matching row (true) - * or the entire result set (in case of multiple matches)? - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return mixed Matching row/result set if found or created, null otherwise. - */ - public function getByText($tag, $create = true, $firstOnly = true, $caseSensitive = null) - { - $cs = $caseSensitive ?? $this->caseSensitive; - $result = $this->getDbService(TagServiceInterface::class)->getTagsByText($tag, $cs); - if (count($result) == 0 && $create) { - $row = $this->createRow(); - $row->tag = $cs ? $tag : mb_strtolower($tag, 'UTF8'); - $row->save(); - return $firstOnly ? $row : [$row]; - } - return $firstOnly ? $result[0] ?? null : $result; - } - - /** - * Get the tags that match a string - * - * @param string $text Tag to look up. - * @param string $sort Sort/search parameter - * @param int $limit Maximum number of tags - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return array Array of \VuFind\Db\Row\Tags objects - */ - public function matchText($text, $sort = 'alphabetical', $limit = 100, $caseSensitive = null) - { - $callback = function ($select) use ($text) { - $select->where->literal('lower(tag) like lower(?)', [$text . '%']); - // Discard tags assigned to a user list. - $select->where->isNotNull('resource_tags.resource_id'); - }; - return $this->getTagList($sort, $limit, $callback, $caseSensitive); - } - - /** - * Get all resources associated with the provided tag query. - * - * @param string $q Search query - * @param string $source Record source (optional limiter) - * @param string $sort Resource field to sort on (optional) - * @param int $offset Offset for results - * @param int $limit Limit for results (null for none) - * @param bool $fuzzy Are we doing an exact or fuzzy search? - * @param ?bool $caseSensitive Should search be case sensitive? (null to use configured default) - * - * @return array - */ - public function resourceSearch( - $q, - $source = null, - $sort = null, - $offset = 0, - $limit = null, - $fuzzy = true, - $caseSensitive = null - ) { - $cb = function ($select) use ($q, $source, $sort, $offset, $limit, $fuzzy, $caseSensitive) { - $columns = [ - new Expression( - 'DISTINCT(?)', - ['resource.id'], - [Expression::TYPE_IDENTIFIER] - ), - ]; - $select->columns($columns); - $select->join( - ['rt' => 'resource_tags'], - 'tags.id = rt.tag_id', - [] - ); - $select->join( - ['resource' => 'resource'], - 'rt.resource_id = resource.id', - Select::SQL_STAR - ); - if ($fuzzy) { - $select->where->literal('lower(tags.tag) like lower(?)', [$q]); - } elseif (!($caseSensitive ?? $this->caseSensitive)) { - $select->where->literal('lower(tags.tag) = lower(?)', [$q]); - } else { - $select->where->equalTo('tags.tag', $q); - } - // Discard tags assigned to a user list. - $select->where->isNotNull('rt.resource_id'); - - if (!empty($source)) { - $select->where->equalTo('source', $source); - } - - if (!empty($sort)) { - Resource::applySort($select, $sort, 'resource', $columns); - } - - if ($offset > 0) { - $select->offset($offset); - } - if (null !== $limit) { - $select->limit($limit); - } - }; - - return $this->select($cb); - } - - /** - * Get tags associated with the specified resource. - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * @param int $limit Max. number of tags to return (0 = no limit) - * @param int $list ID of list to load tags from (null for no - * restriction, true for on ANY list, false for on NO list) - * @param int $user ID of user to load tags from (null for all users) - * @param string $sort Sort type ('count' or 'tag') - * @param int $userToCheck ID of user to check for ownership (this will - * not filter the result list, but rows owned by this user will have an is_me - * column set to 1) - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return array - */ - public function getForResource( - $id, - $source = DEFAULT_SEARCH_BACKEND, - $limit = 0, - $list = null, - $user = null, - $sort = 'count', - $userToCheck = null, - $caseSensitive = null - ) { - return $this->select( - function ($select) use ( - $id, - $source, - $limit, - $list, - $user, - $sort, - $userToCheck, - $caseSensitive - ) { - // If we're looking for ownership, create sub query to merge in - // an "is_me" flag value if the selected resource is tagged by - // the specified user. - if (!empty($userToCheck)) { - $subq = $this->getIsMeSubquery($id, $source, $userToCheck); - $select->join( - ['subq' => $subq], - 'tags.id = subq.tag_id', - [ - // is_me will either be null (not owned) or the ID - // of the tag (owned by the current user). - 'is_me' => new Expression( - 'MAX(?)', - ['subq.tag_id'], - [Expression::TYPE_IDENTIFIER] - ), - ], - Select::JOIN_LEFT - ); - } - // SELECT (do not add table prefixes) - $select->columns( - [ - 'id', - 'tag' => ($caseSensitive ?? $this->caseSensitive) - ? 'tag' : new Expression('lower(tag)'), - 'cnt' => new Expression( - 'COUNT(DISTINCT(?))', - ['rt.user_id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['rt' => 'resource_tags'], - 'rt.tag_id = tags.id', - [] - ); - $select->join( - ['r' => 'resource'], - 'rt.resource_id = r.id', - [] - ); - $select->where(['r.record_id' => $id, 'r.source' => $source]); - $select->group(['tags.id', 'tag']); - - if ($sort == 'count') { - $select->order(['cnt DESC', new Expression('lower(tags.tag)')]); - } elseif ($sort == 'tag') { - $select->order([new Expression('lower(tags.tag)')]); - } - - if ($limit > 0) { - $select->limit($limit); - } - if ($list === true) { - $select->where->isNotNull('rt.list_id'); - } elseif ($list === false) { - $select->where->isNull('rt.list_id'); - } elseif (null !== $list) { - $select->where->equalTo('rt.list_id', $list); - } - if (null !== $user) { - $select->where->equalTo('rt.user_id', $user); - } - } - ); - } - - /** - * Get a list of all tags generated by the user in favorites lists. Note that - * the returned list WILL NOT include tags attached to records that are not - * saved in favorites lists. - * - * @param string $userId User ID to look up. - * @param string $resourceId Filter for tags tied to a specific resource (null for no filter). - * @param int $listId Filter for tags tied to a specific list (null for no filter). - * @param string $source Filter for tags tied to a specific record source (null for no filter). - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getListTagsForUser( - $userId, - $resourceId = null, - $listId = null, - $source = null, - $caseSensitive = null - ) { - $callback = function ($select) use ($userId, $resourceId, $listId, $source, $caseSensitive) { - $select->columns( - [ - 'id' => new Expression( - 'min(?)', - ['tags.id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag' => ($caseSensitive ?? $this->caseSensitive) - ? 'tag' : new Expression('lower(tag)'), - 'cnt' => new Expression( - 'COUNT(DISTINCT(?))', - ['rt.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - ['rt' => 'resource_tags'], - 'tags.id = rt.tag_id', - [] - ); - $select->join( - ['r' => 'resource'], - 'rt.resource_id = r.id', - [] - ); - $select->join( - ['ur' => 'user_resource'], - 'r.id = ur.resource_id', - [] - ); - $select->group(['tag'])->order([new Expression('lower(tag)')]); - - $select->where->equalTo('ur.user_id', $userId) - ->equalTo('rt.user_id', $userId) - ->equalTo( - 'ur.list_id', - 'rt.list_id', - Predicate::TYPE_IDENTIFIER, - Predicate::TYPE_IDENTIFIER - ); - - if (null !== $source) { - $select->where->equalTo('r.source', $source); - } - - if (null !== $resourceId) { - $select->where->equalTo('r.record_id', $resourceId); - } - if (null !== $listId) { - $select->where->equalTo('rt.list_id', $listId); - } - }; - return $this->select($callback); - } - - /** - * Get tags assigned to a user list. - * - * @param int $listId List ID - * @param ?int $userId User ID to look up (null for no filter). - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getForList($listId, $userId = null, $caseSensitive = null) - { - $callback = function ($select) use ($listId, $userId, $caseSensitive) { - $select->columns( - [ - 'id' => new Expression( - 'min(?)', - ['tags.id'], - [Expression::TYPE_IDENTIFIER] - ), - 'tag' => ($caseSensitive ?? $this->caseSensitive) - ? 'tag' : new Expression('lower(tag)'), - ] - ); - $select->join( - ['rt' => 'resource_tags'], - 'tags.id = rt.tag_id', - [] - ); - $select->where->equalTo('rt.list_id', $listId); - $select->where->isNull('rt.resource_id'); - if ($userId) { - $select->where->equalTo('rt.user_id', $userId); - } - $select->group(['tag'])->order([new Expression('lower(tag)')]); - }; - return $this->select($callback); - } - - /** - * Get a subquery used for flagging tag ownership (see getForResource). - * - * @param string $id Record ID to look up - * @param string $source Source of record to look up - * @param int $userToCheck ID of user to check for ownership - * - * @return Select - */ - protected function getIsMeSubquery($id, $source, $userToCheck) - { - $sub = new Select('resource_tags'); - $sub->columns(['tag_id']) - ->join( - // Convert record_id to resource_id - ['r' => 'resource'], - 'resource_id = r.id', - [] - ) - ->where( - [ - 'r.record_id' => $id, - 'r.source' => $source, - 'user_id' => $userToCheck, - ] - ); - return $sub; - } - - /** - * Get a list of tags based on a sort method ($sort) - * - * @param string $sort Sort/search parameter - * @param int $limit Maximum number of tags (default = 100, < 1 = no limit) - * @param callback $extra_where Extra code to modify $select (null for none) - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return array Tag details. - */ - public function getTagList($sort, $limit = 100, $extra_where = null, $caseSensitive = null) - { - $callback = function ($select) use ($sort, $limit, $extra_where, $caseSensitive) { - $select->columns( - [ - 'id', - 'tag' => ($caseSensitive ?? $this->caseSensitive) - ? 'tag' : new Expression('lower(tag)'), - 'cnt' => new Expression( - 'COUNT(DISTINCT(?))', - ['resource_tags.resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'posted' => new Expression( - 'MAX(?)', - ['resource_tags.posted'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->join( - 'resource_tags', - 'tags.id = resource_tags.tag_id', - [] - ); - if (is_callable($extra_where)) { - $extra_where($select); - } - $select->group(['tags.id', 'tags.tag']); - switch ($sort) { - case 'alphabetical': - $select->order([new Expression('lower(tags.tag)'), 'cnt DESC']); - break; - case 'popularity': - $select->order(['cnt DESC', new Expression('lower(tags.tag)')]); - break; - case 'recent': - $select->order( - [ - 'posted DESC', - 'cnt DESC', - new Expression('lower(tags.tag)'), - ] - ); - break; - } - // Limit the size of our results - if ($limit > 0) { - $select->limit($limit); - } - }; - - $tagList = []; - foreach ($this->select($callback) as $t) { - $tagList[] = [ - 'tag' => $t->tag, - 'cnt' => $t->cnt, - ]; - } - return $tagList; - } - - /** - * Delete a group of tags. - * - * @param array $ids IDs of tags to delete. - * - * @return void - */ - public function deleteByIdArray($ids) - { - // Do nothing if we have no IDs to delete! - if (empty($ids)) { - return; - } - - $callback = function ($select) use ($ids) { - $select->where->in('id', $ids); - }; - $this->delete($callback); - } - - /** - * Get a list of duplicate tags (this should never happen, but past bugs - * and the introduction of case-insensitive tags have introduced problems). - * - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return mixed - */ - public function getDuplicates($caseSensitive = null) - { - $callback = function ($select) use ($caseSensitive) { - $select->columns( - [ - 'tag' => new Expression( - 'MIN(?)', - ['tag'], - [Expression::TYPE_IDENTIFIER] - ), - 'cnt' => new Expression( - 'COUNT(?)', - ['tag'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MIN(?)', - ['id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->group( - ($caseSensitive ?? $this->caseSensitive) ? 'tag' : new Expression('lower(tag)') - ); - $select->having('COUNT(tag) > 1'); - }; - return $this->select($callback); - } - - /** - * Support method for fixDuplicateTag() -- merge $source into $target. - * - * @param string $target Target ID - * @param string $source Source ID - * - * @return void - */ - protected function mergeTags($target, $source) - { - // Don't merge a tag with itself! - if ($target === $source) { - return; - } - $table = $this->getDbTable('ResourceTags'); - $resourceTagsService = $this->getDbService(ResourceTagsServiceInterface::class); - $result = $table->select(['tag_id' => $source]); - - foreach ($result as $current) { - // Move the link to the target ID: - $resourceTagsService->createLink( - $current->resource_id, - $target, - $current->user_id, - $current->list_id, - $current->getPosted() - ); - - // Remove the duplicate link: - $table->delete($current->toArray()); - } - - // Remove the source tag: - $this->delete(['id' => $source]); - } - - /** - * Support method for fixDuplicateTags() - * - * @param string $tag Tag to deduplicate. - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return void - */ - protected function fixDuplicateTag($tag, $caseSensitive = null) - { - // Make sure this really is a duplicate. - $result = $this->getDbService(TagServiceInterface::class) - ->getTagsByText($tag, $caseSensitive ?? $this->caseSensitive); - if (count($result) < 2) { - return; - } - - $first = $result[0]; - foreach ($result as $current) { - $this->mergeTags($first->getId(), $current->getId()); - } - } - - /** - * Repair duplicate tags in the database (if any). - * - * @param ?bool $caseSensitive Should tags be case sensitive? (null to use configured default) - * - * @return void - */ - public function fixDuplicateTags($caseSensitive = null) - { - foreach ($this->getDuplicates($caseSensitive) as $dupe) { - $this->fixDuplicateTag($dupe->tag, $caseSensitive); - } - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/User.php b/module/VuFind/src/VuFind/Db/Table/User.php deleted file mode 100644 index 8d5a2f40361..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/User.php +++ /dev/null @@ -1,190 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Session\Container; -use VuFind\Config\Config; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Row\User as UserRow; - -/** - * Table Definition for user - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site - */ -class User extends Gateway -{ - /** - * VuFind configuration - * - * @var Config - */ - protected $config; - - /** - * Session container - * - * @var Container - */ - protected $session; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param Config $config VuFind configuration - * @param ?Container $session Session container to inject into rows - * (optional; used for privacy mode) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj, - Config $config, - ?Container $session = null, - $table = 'user' - ) { - $this->config = $config; - $this->session = $session; - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Create a row for the specified username. - * - * @param string $username Username - * - * @return UserRow - */ - public function createRowForUsername($username) - { - $row = $this->createRow(); - $row->username = $username; - $row->created = date('Y-m-d H:i:s'); - // Failing to initialize this here can cause Laminas\Db errors in - // the VuFind\Auth\Shibboleth and VuFind\Auth\ILS integration tests. - $row->user_provided_email = 0; - return $row; - } - - /** - * Retrieve a user object from the database based on ID. - * - * @param int $id ID. - * - * @return ?UserRow - */ - public function getById($id) - { - return $this->select(['id' => $id])->current(); - } - - /** - * Retrieve a user object from the database based on catalog ID. - * - * @param string $catId Catalog ID. - * - * @return ?UserRow - */ - public function getByCatalogId($catId) - { - return $this->select(['cat_id' => $catId])->current(); - } - - /** - * Retrieve a user object from the database based on username; when requested, - * create a new row if no existing match is found. - * - * @param string $username Username to use for retrieval. - * @param bool $create Should we create users that don't already exist? - * - * @return ?UserRow - */ - public function getByUsername($username, $create = true) - { - $callback = function ($select) use ($username) { - $select->where->literal('lower(username) = lower(?)', [$username]); - }; - $row = $this->select($callback)->current(); - return ($create && empty($row)) - ? $this->createRowForUsername($username) : $row; - } - - /** - * Retrieve a user object from the database based on email. - * - * @param string $email email to use for retrieval. - * - * @return ?UserRow - */ - public function getByEmail($email) - { - $row = $this->select(['email' => $email])->current(); - return $row; - } - - /** - * Get user rows with insecure passwords and/or catalog passwords - * - * @return mixed - */ - public function getInsecureRows() - { - $callback = function ($select) { - $select->where - ->notEqualTo('password', '') - ->OR->isNotNull('cat_password'); - }; - return $this->select($callback); - } - - /** - * Return a row by a verification hash - * - * @param string $hash User-unique hash string - * - * @return ?UserRow - */ - public function getByVerifyHash($hash) - { - $row = $this->select(['verify_hash' => $hash])->current(); - return $row; - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/UserCard.php b/module/VuFind/src/VuFind/Db/Table/UserCard.php deleted file mode 100644 index 50a21b842cc..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/UserCard.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; - -/** - * Table Definition for user_card - * - * @category VuFind - * @package Db_Table - * @author Ere Maijala - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ -class UserCard extends Gateway -{ - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'user_card' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get user_card rows with insecure catalog passwords - * - * @return mixed - */ - public function getInsecureRows() - { - $callback = function ($select) { - $select->where->isNotNull('cat_password'); - }; - return $this->select($callback); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/UserList.php b/module/VuFind/src/VuFind/Db/Table/UserList.php deleted file mode 100644 index bbf40c432ea..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/UserList.php +++ /dev/null @@ -1,170 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use Laminas\Session\Container; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Exception\LoginRequired as LoginRequiredException; -use VuFind\Exception\RecordMissing as RecordMissingException; - -/** - * Table Definition for user_list - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ -class UserList extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Session container for last list information. - * - * @var Container - */ - protected $session; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param ?Container $session Session container (must use same - * namespace as container provided to \VuFind\View\Helper\Root\UserList). - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - ?Container $session = null, - $table = 'user_list' - ) { - $this->session = $session; - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Create a new list object. - * - * @param \VuFind\Db\Row\UserList|bool $user User object representing owner of - * new list (or false if not logged in) - * - * @return \VuFind\Db\Row\UserList - * @throws LoginRequiredException - * - * @deprecated Use \VuFind\Favorites\FavoritesService::createListForUser() - */ - public function getNew($user) - { - if (!$user) { - throw new LoginRequiredException('Log in to create lists.'); - } - - $row = $this->createRow(); - $row->created = date('Y-m-d H:i:s'); // force creation date - $row->user_id = $user->id; - return $row; - } - - /** - * Retrieve a list object. - * - * @param int $id Numeric ID for existing list. - * - * @return \VuFind\Db\Row\UserList - * @throws RecordMissingException - * - * @deprecated Use \VuFind\Db\Service\UserListServiceInterface::getUserListById() - */ - public function getExisting($id) - { - return $this->getDbService(UserListServiceInterface::class)->getUserListById($id); - } - - /** - * Get lists containing a specific user_resource - * - * @param string $resourceId ID of record being checked. - * @param string $source Source of record to look up - * @param int $userId Optional user ID (to limit results to a particular - * user). - * - * @return array - */ - public function getListsContainingResource( - $resourceId, - $source = DEFAULT_SEARCH_BACKEND, - $userId = null - ) { - // Set up base query: - $callback = function ($select) use ($resourceId, $source, $userId) { - $select->columns( - [ - new Expression( - 'DISTINCT(?)', - ['user_list.id'], - [Expression::TYPE_IDENTIFIER] - ), Select::SQL_STAR, - ] - ); - $select->join( - ['ur' => 'user_resource'], - 'ur.list_id = user_list.id', - [] - ); - $select->join( - ['r' => 'resource'], - 'r.id = ur.resource_id', - [] - ); - $select->where->equalTo('r.source', $source) - ->equalTo('r.record_id', $resourceId); - $select->order(['title']); - - if (null !== $userId) { - $select->where->equalTo('ur.user_id', $userId); - } - }; - return $this->select($callback); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/UserListFactory.php b/module/VuFind/src/VuFind/Db/Table/UserListFactory.php deleted file mode 100644 index aad665811a0..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/UserListFactory.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ - -namespace VuFind\Db\Table; - -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; -use Laminas\ServiceManager\Exception\ServiceNotFoundException; -use Psr\Container\ContainerExceptionInterface as ContainerException; -use Psr\Container\ContainerInterface; - -/** - * UserList table gateway factory. - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development Wiki - */ -class UserListFactory extends GatewayFactory -{ - /** - * Create an object - * - * @param ContainerInterface $container Service manager - * @param string $requestedName Service being created - * @param null|array $options Extra options (optional) - * - * @return object - * - * @throws ServiceNotFoundException if unable to resolve the service. - * @throws ServiceNotCreatedException if an exception is raised when - * creating a service. - * @throws ContainerException&\Throwable if any other error occurs - */ - public function __invoke( - ContainerInterface $container, - $requestedName, - ?array $options = null - ) { - if (!empty($options)) { - throw new \Exception('Unexpected options sent to factory!'); - } - return parent::__invoke($container, $requestedName, [null]); - } -} diff --git a/module/VuFind/src/VuFind/Db/Table/UserResource.php b/module/VuFind/src/VuFind/Db/Table/UserResource.php deleted file mode 100644 index 9e7e93eb2b7..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/UserResource.php +++ /dev/null @@ -1,314 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ - -namespace VuFind\Db\Table; - -use Laminas\Db\Adapter\Adapter; -use Laminas\Db\Sql\Expression; -use Laminas\Db\Sql\Select; -use VuFind\Db\Row\RowGateway; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\DbServiceAwareTrait; -use VuFind\Db\Service\ResourceTagsServiceInterface; -use VuFind\Db\Service\UserResourceServiceInterface; - -/** - * Table Definition for user_resource - * - * @category VuFind - * @package Db_Table - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page - */ -class UserResource extends Gateway implements DbServiceAwareInterface -{ - use DbServiceAwareTrait; - - /** - * Constructor - * - * @param Adapter $adapter Database adapter - * @param PluginManager $tm Table manager - * @param array $cfg Laminas configuration - * @param ?RowGateway $rowObj Row prototype object (null for default) - * @param string $table Name of database table to interface with - */ - public function __construct( - Adapter $adapter, - PluginManager $tm, - $cfg, - ?RowGateway $rowObj = null, - $table = 'user_resource' - ) { - parent::__construct($adapter, $tm, $cfg, $rowObj, $table); - } - - /** - * Get information saved in a user's favorites for a particular record. - * - * @param string $resourceId ID of record being checked. - * @param string $source Source of record to look up - * @param int $listId Optional list ID (to limit results to a particular - * list). - * @param int $userId Optional user ID (to limit results to a particular - * user). - * - * @return \Laminas\Db\ResultSet\AbstractResultSet - */ - public function getSavedData( - $resourceId, - $source = DEFAULT_SEARCH_BACKEND, - $listId = null, - $userId = null - ) { - $callback = function ($select) use ($resourceId, $source, $listId, $userId) { - $select->columns( - [ - new Expression( - 'DISTINCT(?)', - ['user_resource.id'], - [Expression::TYPE_IDENTIFIER] - ), Select::SQL_STAR, - ] - ); - $select->join( - ['r' => 'resource'], - 'r.id = user_resource.resource_id', - [] - ); - $select->join( - ['ul' => 'user_list'], - 'user_resource.list_id = ul.id', - ['list_title' => 'title', 'list_id' => 'id'] - ); - $select->where->equalTo('r.source', $source) - ->equalTo('r.record_id', $resourceId); - - if (null !== $userId) { - $select->where->equalTo('user_resource.user_id', $userId); - } - if (null !== $listId) { - $select->where->equalTo('user_resource.list_id', $listId); - } - }; - return $this->select($callback); - } - - /** - * Create link if one does not exist; update notes if one does. - * - * @param string $resource_id ID of resource to link up - * @param string $user_id ID of user creating link - * @param string $list_id ID of list to link up - * @param string $notes Notes to associate with link - * - * @return \VuFind\Db\Row\UserResource - * - * @deprecated Use UserResourceServiceInterface::createOrUpdateLink() - */ - public function createOrUpdateLink( - $resource_id, - $user_id, - $list_id, - $notes = '' - ) { - return $this->getDbService(UserResourceServiceInterface::class) - ->createOrUpdateLink($resource_id, $user_id, $list_id, $notes); - } - - /** - * Unlink rows for the specified resource. This will also automatically remove - * any tags associated with the relationship. - * - * @param string|array $resource_id ID (or array of IDs) of resource(s) to - * unlink (null for ALL matching resources) - * @param string $user_id ID of user removing links - * @param string $list_id ID of list to unlink - * (null for ALL matching lists, with the destruction of all tags associated - * with the $resource_id value; true for ALL matching lists, but retaining - * any tags associated with the $resource_id independently of lists) - * - * @return void - * - * @deprecated - */ - public function destroyLinks($resource_id, $user_id, $list_id = null) - { - // Remove any tags associated with the links we are removing; we don't - // want to leave orphaned tags in the resource_tags table after we have - // cleared out favorites in user_resource! - $resourceTagsService = $this->getDbService(ResourceTagsServiceInterface::class); - if ($list_id === true) { - $resourceTagsService->destroyAllListResourceTagsLinksForUser($resource_id, $user_id); - } else { - $resourceTagsService->destroyResourceTagsLinksForUser($resource_id, $user_id, $list_id); - } - - // Now build the where clause to figure out which rows to remove: - $callback = function ($select) use ($resource_id, $user_id, $list_id) { - $select->where->equalTo('user_id', $user_id); - if (null !== $resource_id) { - $select->where->in('resource_id', (array)$resource_id); - } - // null or true values of $list_id have different meanings in the - // context of the destroyResourceTagsLinksForUser() call above, since - // some tags have a null $list_id value. In the case of user_resource - // rows, however, every row has a non-null $list_id value, so the - // two cases are equivalent and may be handled identically. - if (null !== $list_id && true !== $list_id) { - $select->where->equalTo('list_id', $list_id); - } - }; - - // Delete the rows: - $this->delete($callback); - } - - /** - * Get statistics on use of lists. - * - * @return array - */ - public function getStatistics() - { - $select = $this->sql->select(); - $select->columns( - [ - 'users' => new Expression( - 'COUNT(DISTINCT(?))', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'lists' => new Expression( - 'COUNT(DISTINCT(?))', - ['list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'resources' => new Expression( - 'COUNT(DISTINCT(?))', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'total' => new Expression('COUNT(*)'), - ] - ); - $statement = $this->sql->prepareStatementForSqlObject($select); - $result = $statement->execute(); - return (array)$result->current(); - } - - /** - * Get a list of duplicate rows (this sometimes happens after merging IDs, - * for example after a Summon resource ID changes). - * - * @return mixed - */ - public function getDuplicates() - { - $callback = function ($select) { - $select->columns( - [ - 'resource_id' => new Expression( - 'MIN(?)', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'list_id' => new Expression( - 'MIN(?)', - ['list_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'user_id' => new Expression( - 'MIN(?)', - ['user_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'cnt' => new Expression( - 'COUNT(?)', - ['resource_id'], - [Expression::TYPE_IDENTIFIER] - ), - 'id' => new Expression( - 'MIN(?)', - ['id'], - [Expression::TYPE_IDENTIFIER] - ), - ] - ); - $select->group(['resource_id', 'list_id', 'user_id']); - $select->having('COUNT(resource_id) > 1'); - }; - return $this->select($callback); - } - - /** - * Deduplicate rows (sometimes necessary after merging foreign key IDs). - * - * @return void - */ - public function deduplicate() - { - foreach ($this->getDuplicates() as $dupe) { - // Do this as a transaction to prevent odd behavior: - $connection = $this->getAdapter()->getDriver()->getConnection(); - $connection->beginTransaction(); - - // Merge notes together... - $mainCriteria = [ - 'resource_id' => $dupe['resource_id'], - 'list_id' => $dupe['list_id'], - 'user_id' => $dupe['user_id'], - ]; - $dupeRows = $this->select($mainCriteria); - $notes = []; - foreach ($dupeRows as $row) { - if (!empty($row['notes'])) { - $notes[] = $row['notes']; - } - } - $this->update( - ['notes' => implode(' ', $notes)], - ['id' => $dupe['id']] - ); - // Now delete extra rows... - $callback = function ($select) use ($dupe, $mainCriteria) { - // match on all relevant IDs in duplicate group - $select->where($mainCriteria); - // getDuplicates returns the minimum id in the set, so we want to - // delete all of the duplicates with a higher id value. - $select->where->greaterThan('id', $dupe['id']); - }; - $this->delete($callback); - - // Done -- commit the transaction: - $connection->commit(); - } - } -} diff --git a/module/VuFind/src/VuFindTest/Container/MockDbTablePluginManager.php b/module/VuFind/src/VuFind/Exception/DuplicateKeyException.php similarity index 71% rename from module/VuFind/src/VuFindTest/Container/MockDbTablePluginManager.php rename to module/VuFind/src/VuFind/Exception/DuplicateKeyException.php index bf420f56ae0..119c5642e9e 100644 --- a/module/VuFind/src/VuFindTest/Container/MockDbTablePluginManager.php +++ b/module/VuFind/src/VuFind/Exception/DuplicateKeyException.php @@ -1,11 +1,11 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page + * @link https://vufind.org/wiki/development Wiki */ -namespace VuFindTest\Container; +namespace VuFind\Exception; /** - * DB table plugin container that produces mock objects. + * Duplicate Key Exception * * @category VuFind - * @package Tests + * @package Exceptions * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Page + * @link https://vufind.org/wiki/development Wiki */ -class MockDbTablePluginManager extends \VuFind\Db\Table\PluginManager +class DuplicateKeyException extends \Exception { - use MockContainerTrait; } diff --git a/module/VuFind/src/VuFind/Form/Handler/Database.php b/module/VuFind/src/VuFind/Form/Handler/Database.php index a5b019cf670..453b77e3aa6 100644 --- a/module/VuFind/src/VuFind/Form/Handler/Database.php +++ b/module/VuFind/src/VuFind/Form/Handler/Database.php @@ -35,6 +35,7 @@ use Laminas\Log\LoggerAwareInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Service\FeedbackServiceInterface; +use VuFind\Db\Service\UserService; use VuFind\Log\LoggerAwareTrait; /** @@ -54,10 +55,12 @@ class Database implements HandlerInterface, LoggerAwareInterface * Constructor * * @param FeedbackServiceInterface $feedbackService Feedback database service + * @param UserService $userService User database service * @param string $baseUrl Site base url */ public function __construct( protected FeedbackServiceInterface $feedbackService, + protected UserService $userService, protected string $baseUrl ) { } diff --git a/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php b/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php index d64b8e7437f..ea34991f824 100644 --- a/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php +++ b/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php @@ -78,6 +78,7 @@ public function __invoke( $baseUrl = $serverUrl($router->assemble([], ['name' => 'home'])); return new $requestedName( $dbServiceManager->get(\VuFind\Db\Service\FeedbackServiceInterface::class), + $dbServiceManager->get(\VuFind\Db\Service\UserService::class), $baseUrl ); } diff --git a/module/VuFind/src/VuFind/Log/LoggerFactory.php b/module/VuFind/src/VuFind/Log/LoggerFactory.php index 6ea7aa8b41a..e92e132c652 100644 --- a/module/VuFind/src/VuFind/Log/LoggerFactory.php +++ b/module/VuFind/src/VuFind/Log/LoggerFactory.php @@ -85,7 +85,7 @@ protected function addDbWriters( // Make Writers $filters = explode(',', $error_types); $writer = new Writer\Db( - $container->get(\Laminas\Db\Adapter\Adapter::class), + $container->get(\VuFind\Db\Connection::class), $table_name, $columnMapping ); diff --git a/module/VuFind/src/VuFind/Log/Writer/Db.php b/module/VuFind/src/VuFind/Log/Writer/Db.php index c3e66e610bc..e8e2c52bdbb 100644 --- a/module/VuFind/src/VuFind/Log/Writer/Db.php +++ b/module/VuFind/src/VuFind/Log/Writer/Db.php @@ -5,7 +5,7 @@ * * PHP version 8 * - * Copyright (C) Villanova University 2010. + * Copyright (C) Villanova University 2010-2025. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -23,25 +23,103 @@ * @category VuFind * @package Error_Logging * @author Chris Hallberg + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ namespace VuFind\Log\Writer; +use Doctrine\DBAL\Connection; +use Laminas\Log\Exception; +use Laminas\Log\Formatter\Db as DbFormatter; + +use function is_array; +use function is_scalar; +use function var_export; + /** - * This class extends the Laminas Logging towards DB + * This class is heavily based on \Laminas\Log\Writer\Db, but replaces Laminas\Db + * functionality with Doctrine\DBAL. * * @category VuFind * @package Error_Logging * @author Chris Hallberg + * @author Demian Katz * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org Main Site */ -class Db extends \Laminas\Log\Writer\Db +class Db extends \Laminas\Log\Writer\AbstractWriter { use VerbosityTrait; + /** + * Constructor + * + * @param Connection $db Db adapter instance + * @param string $tableName Table name + * @param array $columnMap Relates database columns names to log data field keys. + * @param string $separator Field separator for sub-elements + */ + public function __construct( + protected Connection $db, + protected string $tableName, + protected array $columnMap, + protected string $separator = '_' + ) { + $this->setFormatter(new DbFormatter()); + } + + /** + * Write a message to the log. + * + * @param array $event Event data + * + * @return void + * @throws Exception\RuntimeException + */ + protected function doDatabaseWrite(array $event) + { + $event = $this->formatter->format($event); + + $dataToInsert = $this->mapEventIntoColumn($event, $this->columnMap); + $this->db->insert($this->tableName, $dataToInsert); + } + + /** + * Map event into column using the $columnMap array + * + * @param array $event Event to map + * @param ?array $columnMap Column map + * + * @return array + */ + protected function mapEventIntoColumn(array $event, ?array $columnMap = null) + { + if (empty($event)) { + return []; + } + + $data = []; + foreach ($event as $name => $value) { + if (is_array($value)) { + foreach ($value as $key => $subvalue) { + if (isset($columnMap[$name][$key])) { + if (is_scalar($subvalue)) { + $data[$columnMap[$name][$key]] = $subvalue; + continue; + } + + $data[$columnMap[$name][$key]] = var_export($subvalue, true); + } + } + } elseif (isset($columnMap[$name])) { + $data[$columnMap[$name]] = $value; + } + } + return $data; + } + /** * Write a message to the log. * @@ -53,6 +131,6 @@ class Db extends \Laminas\Log\Writer\Db protected function doWrite(array $event) { // Apply verbosity, Call parent method: - parent::doWrite($this->applyVerbosity($event)); + $this->doDatabaseWrite($this->applyVerbosity($event)); } } diff --git a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php index 1d91cd8fa64..c432c9a586e 100644 --- a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php +++ b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php @@ -30,7 +30,6 @@ namespace VuFind\RecordDriver; use VuFind\Db\Service\CommentsServiceInterface; -use VuFind\Db\Service\TagServiceInterface; use VuFind\Db\Service\UserListServiceInterface; use VuFind\XSLT\Import\VuFind as ArticleStripper; @@ -49,12 +48,10 @@ */ abstract class AbstractBase implements \VuFind\Db\Service\DbServiceAwareInterface, - \VuFind\Db\Table\DbTableAwareInterface, \VuFind\I18n\Translator\TranslatorAwareInterface, \VuFindSearch\Response\RecordInterface { use \VuFind\Db\Service\DbServiceAwareTrait; - use \VuFind\Db\Table\DbTableAwareTrait; use \VuFind\I18n\Translator\TranslatorAwareTrait; use \VuFindSearch\Response\RecordTrait; @@ -178,36 +175,6 @@ public function getSortTitle() return ArticleStripper::stripArticles($this->getBreadcrumb()); } - /** - * Get tags associated with this record. - * - * @param int $list_id ID of list to load tags from (null for all lists) - * @param int $user_id ID of user to load tags from (null for all users) - * @param string $sort Sort type ('count' or 'tag') - * @param int $ownerId ID of user to check for ownership - * - * @return array - * - * @deprecated Use TagServiceInterface::getRecordTags() or TagServiceInterface::getRecordTagsFromFavorites() - * or TagServiceInterface::getRecordTagsNotInFavorites() - */ - public function getTags( - $list_id = null, - $user_id = null, - $sort = 'count', - $ownerId = null - ) { - return $this->getDbTable('Tags')->getForResource( - $this->getUniqueId(), - $this->getSourceIdentifier(), - 0, - $list_id, - $user_id, - $sort, - $ownerId - ); - } - /** * Get rating information for this record. * @@ -264,58 +231,6 @@ public function getRatingBreakdown(array $groups) ); } - /** - * Add or update user's rating for the record. - * - * @param int $userId ID of the user posting the rating - * @param ?int $rating The user-provided rating, or null to clear any existing - * rating - * - * @return void - * - * @deprecated Use \VuFind\Ratings\RatingsService::saveRating() - */ - public function addOrUpdateRating(int $userId, ?int $rating): void - { - // Clear rating cache: - $this->ratingCache = []; - $resources = $this->getDbTable('Resource'); - $resource = $resources->findResource( - $this->getUniqueId(), - $this->getSourceIdentifier() - ); - $this->getDbService(\VuFind\Db\Service\RatingsServiceInterface::class) - ->addOrUpdateRating($resource, $userId, $rating); - } - - /** - * Get notes associated with this record in user lists. - * - * @param int $list_id ID of list to load tags from (null for all lists) - * @param int $user_id ID of user to load tags from (null for all users) - * - * @return array - * - * @deprecated Use \VuFind\View\Helper\Root\Record::getListNotes() - */ - public function getListNotes($list_id = null, $user_id = null) - { - $db = $this->getDbTable('UserResource'); - $data = $db->getSavedData( - $this->getUniqueId(), - $this->getSourceIdentifier(), - $list_id, - $user_id - ); - $notes = []; - foreach ($data as $current) { - if (!empty($current->notes)) { - $notes[] = $current->notes; - } - } - return $notes; - } - /** * Get a list of lists containing this record. * diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/User.php b/module/VuFind/src/VuFind/Role/PermissionProvider/User.php index 33f347f0348..fb0fa4f67ad 100644 --- a/module/VuFind/src/VuFind/Role/PermissionProvider/User.php +++ b/module/VuFind/src/VuFind/Role/PermissionProvider/User.php @@ -81,6 +81,9 @@ public function getPermissions($options) if (!($user = $this->auth->getIdentity())) { return []; } + if (!($user instanceof \VuFind\Db\Entity\UserEntityInterface)) { + throw new \Exception('Unexpected user object provided!'); + } // which user attribute has to match which pattern to get permissions? foreach ((array)$options as $option) { @@ -95,9 +98,13 @@ public function getPermissions($options) if (! preg_match('/^\/.*\/$/', $pattern)) { $pattern = '/' . $pattern . '/'; } - - if (preg_match($pattern, $user[$attribute])) { - return ['loggedin']; + $methodMap = ['cat_id' => 'getCatId', 'cat_username' => 'getCatUsername']; + $method = $methodMap[$attribute] ?? 'get' . ucfirst($attribute); + if (method_exists($user, $method)) { + $userValue = $user->$method(); + if (preg_match($pattern, $userValue)) { + return ['loggedin']; + } } } } diff --git a/module/VuFind/src/VuFind/Search/Tags/Results.php b/module/VuFind/src/VuFind/Search/Tags/Results.php index 633bdd6bff8..69a2fa3c248 100644 --- a/module/VuFind/src/VuFind/Search/Tags/Results.php +++ b/module/VuFind/src/VuFind/Search/Tags/Results.php @@ -135,7 +135,7 @@ protected function performSearch() // Retrieve record drivers for the selected items. $callback = function ($row) { - return ['id' => $row['record_id'], 'source' => $row['source']]; + return ['id' => $row[0]->getRecordId(), 'source' => $row[0]->getSource()]; }; $this->results = $this->recordLoader ->loadBatch(array_map($callback, $results), true); diff --git a/module/VuFind/src/VuFind/ServiceManager/ServiceInitializer.php b/module/VuFind/src/VuFind/ServiceManager/ServiceInitializer.php index 802a00b2187..90c7981a264 100644 --- a/module/VuFind/src/VuFind/ServiceManager/ServiceInitializer.php +++ b/module/VuFind/src/VuFind/ServiceManager/ServiceInitializer.php @@ -84,11 +84,6 @@ protected function isCacheEnabled(ContainerInterface $sm) */ public function __invoke(ContainerInterface $sm, $instance) { - if ($instance instanceof \VuFind\Db\Table\DbTableAwareInterface) { - $instance->setDbTableManager( - $sm->get(\VuFind\Db\Table\PluginManager::class) - ); - } if ($instance instanceof \VuFind\Db\Service\DbServiceAwareInterface) { $instance->setDbServiceManager( $sm->get(\VuFind\Db\Service\PluginManager::class) diff --git a/module/VuFind/src/VuFind/Session/AbstractBase.php b/module/VuFind/src/VuFind/Session/AbstractBase.php index 331d89c68a0..f6992baf7a0 100644 --- a/module/VuFind/src/VuFind/Session/AbstractBase.php +++ b/module/VuFind/src/VuFind/Session/AbstractBase.php @@ -48,9 +48,6 @@ */ abstract class AbstractBase implements HandlerInterface { - use \VuFind\Db\Table\DbTableAwareTrait { - getDbTable as getTable; - } // Note that we intentionally omit the DbServiceAwareInterface above; the service // manager is injected by AbstractBaseFactory explicitly for compatibility with // the secure delegator factory, so we don't need to auto-inject it. diff --git a/module/VuFind/src/VuFind/Session/HandlerInterface.php b/module/VuFind/src/VuFind/Session/HandlerInterface.php index 84f34b90ea2..4cd966ca6ed 100644 --- a/module/VuFind/src/VuFind/Session/HandlerInterface.php +++ b/module/VuFind/src/VuFind/Session/HandlerInterface.php @@ -32,7 +32,6 @@ namespace VuFind\Session; use Laminas\Session\SaveHandler\SaveHandlerInterface; -use VuFind\Db\Table\DbTableAwareInterface; /** * Session handler interface @@ -44,7 +43,7 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:plugins:session_handlers Wiki */ -interface HandlerInterface extends SaveHandlerInterface, DbTableAwareInterface +interface HandlerInterface extends SaveHandlerInterface { /** * Enable session writing (default) diff --git a/module/VuFind/src/VuFind/Session/SecureDelegator.php b/module/VuFind/src/VuFind/Session/SecureDelegator.php index 9bfd6086228..ec074e11d77 100644 --- a/module/VuFind/src/VuFind/Session/SecureDelegator.php +++ b/module/VuFind/src/VuFind/Session/SecureDelegator.php @@ -33,7 +33,6 @@ use VuFind\Cookie\CookieManager; use VuFind\Crypt\BlockCipher; -use VuFind\Db\Table\PluginManager; use function func_get_args; @@ -168,29 +167,6 @@ public function disableWrites() $this->__call(__FUNCTION__, []); } - /** - * Get the plugin manager. Throw an exception if it is missing. - * - * @throws \Exception - * @return PluginManager - */ - public function getDbTableManager() - { - return $this->__call(__FUNCTION__, []); - } - - /** - * Set the plugin manager. - * - * @param PluginManager $manager Plugin manager - * - * @return void - */ - public function setDbTableManager(PluginManager $manager) - { - $this->__call(__FUNCTION__, func_get_args()); - } - /** * Pass calls to non-existing methods to the wrapped Handler * diff --git a/module/VuFind/src/VuFind/Tags/TagsService.php b/module/VuFind/src/VuFind/Tags/TagsService.php index c3bedbc28f0..6a62e5af034 100644 --- a/module/VuFind/src/VuFind/Tags/TagsService.php +++ b/module/VuFind/src/VuFind/Tags/TagsService.php @@ -38,11 +38,10 @@ use VuFind\Db\Service\ResourceTagsServiceInterface; use VuFind\Db\Service\TagServiceInterface; use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; use VuFind\Record\ResourcePopulator; use VuFind\RecordDriver\AbstractBase as RecordDriver; +use function count; use function is_array; /** @@ -54,10 +53,8 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/ Wiki */ -class TagsService implements DbTableAwareInterface +class TagsService { - use DbTableAwareTrait; - /** * Constructor * @@ -212,13 +209,39 @@ public function unlinkTagsFromRecord(RecordDriver $driver, UserEntityInterface $ } /** - * Repair duplicate tags in the database (if any). + * Support method for fixDuplicateTags() + * + * @param string $tag Tag to deduplicate. + * @param bool $caseSensitive Treat tags as case-sensitive? * * @return void */ - public function fixDuplicateTags(): void + protected function fixDuplicateTag($tag, $caseSensitive) + { + // Make sure this really is a duplicate. + $result = $this->tagDbService->getTagsByText($tag, $caseSensitive); + if (count($result) < 2) { + return; + } + + $first = current($result); + foreach ($result as $current) { + $this->tagDbService->mergeTags($first, $current); + } + } + + /** + * Repair duplicate tags in the database (if any). Returns the number of tags merged. + * + * @return int + */ + public function fixDuplicateTags(): int { - $this->getDbTable('Tags')->fixDuplicateTags($this->caseSensitive); + $dupes = $this->getDuplicateTags(); + foreach ($dupes as $dupe) { + $this->fixDuplicateTag($dupe['tag'], $this->caseSensitive); + } + return count($dupes); } /** diff --git a/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php b/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php index a7c1b8d9f29..50b8a586f00 100644 --- a/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php +++ b/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php @@ -33,20 +33,18 @@ use Throwable; use VuFind\Account\UserAccountService; +use VuFind\Db\PersistenceManager; use VuFind\Db\Service\DbServiceInterface; use VuFind\Db\Service\PluginManager as ServiceManager; use VuFind\Db\Service\ResourceTagsServiceInterface; use VuFind\Db\Service\TagServiceInterface; use VuFind\Db\Service\UserListServiceInterface; -use VuFind\Db\Table\Gateway; -use VuFind\Db\Table\PluginManager as TableManager; +use VuFind\Db\Service\UserService; use VuFind\Favorites\FavoritesService; use VuFind\Favorites\FavoritesServiceFactory; use VuFind\Record\ResourcePopulator; use VuFindTest\Container\MockContainer; -use function count; - /** * Mix-in for accessing a real database during testing. * @@ -69,39 +67,141 @@ trait LiveDatabaseTrait public bool $hasLiveDatabaseTrait = true; /** - * Container connected to live database. + * Plugin manager for database services. + * + * @var ?ServiceManager + */ + protected ?ServiceManager $liveDatabaseServiceManager = null; + + /** + * Container with database-related services configured. * * @var ?MockContainer */ protected ?MockContainer $liveDatabaseContainer = null; /** - * Get a real, working table manager. + * Get merged module config for database access. + * + * @return array + */ + protected function getMergedConfig(): array + { + $dm = new \DoctrineModule\Module(); + $dmConfig = $dm->getConfig(); + $dmo = new \DoctrineORMModule\Module(); + $dmoConfig = $dmo->getConfig(); + $vfConfig + = include APPLICATION_PATH . '/module/VuFind/config/module.config.php'; + return array_replace_recursive($dmConfig, $dmoConfig, $vfConfig); + } + + /** + * Set up minimum Doctrine dependencies in the provided container. + * + * @param object $container Container to populate + * + * @return void + */ + protected function addDoctrineDependenciesToContainer($container): void + { + $container->setAlias( + 'doctrine.entitymanager.orm_vufind', + \Doctrine\ORM\EntityManager::class + ); + $container->setAlias( + 'doctrine.connection.orm_vufind', + \VuFind\Db\Connection::class + ); + $connectionFactory = new \VuFind\Db\ConnectionFactory(); + $container->set( + \VuFind\Db\Connection::class, + $connectionFactory($container, \VuFind\Db\Connection::class) + ); + $config = $container->get('config'); + $options = $config['caches']['doctrinemodule.cache.filesystem']['options']; + $options['cache_dir'] + = LOCAL_CACHE_DIR . '/' . $options['cache_dir'] . '_testmode'; + if (!is_dir($options['cache_dir'])) { + mkdir($options['cache_dir'], 0o777, true); + } + $cacheAdapter = new \Laminas\Cache\Storage\Adapter\Filesystem($options); + $cacheAdapter->addPlugin(new \Laminas\Cache\Storage\Plugin\Serializer()); + $container->set( + 'doctrine.cache.filesystem', + new \DoctrineModule\Cache\LaminasStorageCache($cacheAdapter) + ); + $driverFactory = new \DoctrineModule\Service\DriverFactory('orm_default'); + $container->set( + 'doctrine.driver.orm_default', + $driverFactory($container, 'orm_default') + ); + $configFactory + = new \DoctrineORMModule\Service\ConfigurationFactory('orm_vufind'); + $container->set( + 'doctrine.configuration.orm_vufind', + $configFactory($container, 'orm_vufind') + ); + $eventManagerFactory + = new \DoctrineModule\Service\EventManagerFactory('orm_default'); + $container->set( + 'doctrine.eventmanager.orm_default', + $eventManagerFactory($container, 'orm_default') + ); + $entityResolverFactory + = new \DoctrineORMModule\Service\EntityResolverFactory('orm_default'); + $container->set( + 'doctrine.entity_resolver.orm_default', + $entityResolverFactory($container, 'orm_default') + ); + $container->set( + \VuFind\Db\Entity\PluginManager::class, + new \VuFind\Db\Entity\PluginManager($container, []) + ); + $container->set( + \VuFind\Db\Service\PluginManager::class, + new \VuFind\Db\Service\PluginManager($container, []) + ); + $entityManagerFactory = new \VuFind\Db\EntityManagerFactory( + 'orm_vufind' + ); + $container->set( + \Doctrine\ORM\EntityManager::class, + $entityManagerFactory($container, 'orm_vufind') + ); + $container->set( + PersistenceManager::class, + new PersistenceManager($container->get(\Doctrine\ORM\EntityManager::class)) + ); + } + + /** + * Get a container with Doctrine dependencies included + * + * @return \VuFindTest\Container\MockContainer + */ + public function getMockContainerWithDoctrineDependencies() + { + // Set up the bare minimum services to actually load real configs: + $config = $this->getMergedConfig(); + $container = new \VuFindTest\Container\MockContainer($this); + $container->set(\VuFind\Log\Logger::class, $this->createMock(\Laminas\Log\LoggerInterface::class)); + $container->set('config', $config); + $this->addConfigRelatedServicesToContainer($container, moduleConfig: $config); + $this->addDoctrineDependenciesToContainer($container); + return $container; + } + + /** + * Get a container with database-related services configured. * * @return MockContainer */ public function getLiveDatabaseContainer(): MockContainer { if (!$this->liveDatabaseContainer) { - // Set up the bare minimum services to actually load real configs: - $config = include APPLICATION_PATH . '/module/VuFind/config/module.config.php'; - $container = new MockContainer($this); - $this->addConfigRelatedServicesToContainer($container, moduleConfig: $config); - $adapterFactory = new \VuFind\Db\AdapterFactory( - $container->get(\VuFind\Config\PluginManager::class)->get('config') - ); - $container->set( - \Laminas\Db\Adapter\Adapter::class, - $adapterFactory->getAdapter() - ); - $container->set('config', $config); + $container = $this->getMockContainerWithDoctrineDependencies(); $container->set(\VuFind\Log\Logger::class, $this->createMock(\Laminas\Log\LoggerInterface::class)); - $container->set( - \VuFind\Db\Row\PluginManager::class, - new \VuFind\Db\Row\PluginManager($container, []) - ); - $liveTableManager = new TableManager($container, []); - $container->set(TableManager::class, $liveTableManager); $liveServiceManager = new ServiceManager($container, []); $container->set(ServiceManager::class, $liveServiceManager); $container->set( @@ -131,16 +231,6 @@ public function getLiveDbServiceManager(): ServiceManager return $this->getLiveDatabaseContainer()->get(ServiceManager::class); } - /** - * Get a real, working table manager. - * - * @return TableManager - */ - public function getLiveTableManager(): TableManager - { - return $this->getLiveDatabaseContainer()->get(TableManager::class); - } - /** * Get a database service. * @@ -163,18 +253,6 @@ public function getFavoritesService(): FavoritesService return $this->getLiveDatabaseContainer()->get(FavoritesService::class); } - /** - * Get a table object. - * - * @param string $table Name of table to load - * - * @return Gateway - */ - public function getTable(string $table): Gateway - { - return $this->getLiveTableManager()->get($table); - } - /** * Static setup support function to fail if there is already data in the * database. We want to ensure a clean state for each test! @@ -201,17 +279,19 @@ protected static function failIfDataExists(?string $failMessage = null): void // server) $checks = [ [ - 'table' => \VuFind\Db\Table\User::class, + 'service' => \VuFind\Db\Service\UserService::class, + 'entity' => \VuFind\Db\Entity\User::class, 'name' => 'users', ], [ - 'table' => \VuFind\Db\Table\Tags::class, + 'service' => \VuFind\Db\Service\TagService::class, + 'entity' => \VuFind\Db\Entity\Tags::class, 'name' => 'tags', ], ]; foreach ($checks as $check) { - $table = $test->getTable($check['table']); - if (count($table->select()) > 0) { + $dbService = $test->getDbService($check['service']); + if ($dbService->getRowCountForTable($check['entity']) > 0) { self::fail( $failMessage ?? "Test cannot run with pre-existing {$check['name']} in database!" ); @@ -245,9 +325,9 @@ protected static function removeUsers(array|string $users): void return; } // Delete test user - $userTable = $test->getTable(\VuFind\Db\Table\User::class); + $userService = $test->getDbService(UserService::class); foreach ((array)$users as $username) { - $user = $userTable->getByUsername($username, false); + $user = $userService->getUserByUsername($username); if (!empty($user)) { $purgeService = new UserAccountService($test->getFavoritesService()); $purgeService->setDbServiceManager($test->getLiveDbServiceManager()); diff --git a/module/VuFind/src/VuFindTest/Unit/SessionHandlerTestCase.php b/module/VuFind/src/VuFindTest/Unit/SessionHandlerTestCase.php index 0c2fb2eddb9..1d643f7ceeb 100644 --- a/module/VuFind/src/VuFindTest/Unit/SessionHandlerTestCase.php +++ b/module/VuFind/src/VuFindTest/Unit/SessionHandlerTestCase.php @@ -44,13 +44,6 @@ */ abstract class SessionHandlerTestCase extends \PHPUnit\Framework\TestCase { - /** - * Mock database tables. - * - * @var \VuFind\Db\Table\PluginManager - */ - protected $tables = false; - /** * Mock database services. * @@ -58,20 +51,6 @@ abstract class SessionHandlerTestCase extends \PHPUnit\Framework\TestCase */ protected $services = false; - /** - * Get mock database plugin manager - * - * @return \VuFind\Db\Table\PluginManager - */ - protected function getTables() - { - if (!$this->tables) { - $this->tables - = new \VuFindTest\Container\MockDbTablePluginManager($this); - } - return $this->tables; - } - /** * Get mock database service plugin manager * @@ -86,18 +65,6 @@ protected function getServices() return $this->services; } - /** - * Set up mock databases for a session handler. - * - * @param SessionHandler $handler Session handler - * - * @return void - */ - protected function injectMockDatabaseTables(SessionHandler $handler) - { - $handler->setDbTableManager($this->getTables()); - } - /** * Set up mock database services for a session handler. * @@ -107,7 +74,6 @@ protected function injectMockDatabaseTables(SessionHandler $handler) */ protected function injectMockDatabaseDependencies(SessionHandler $handler) { - $this->injectMockDatabaseTables($handler); $handler->setDbServiceManager($this->getServices()); } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php index 85d1c2ab4b6..525a1237d07 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php @@ -132,7 +132,6 @@ public function getAuthObject( $this->createMock(\VuFind\Auth\ILSAuthenticator::class) ); $obj->setDbServiceManager($this->getLiveDbServiceManager()); - $obj->setDbTableManager($this->getLiveTableManager()); $obj->setConfig($config); return $obj; } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/ChangeTrackerServiceTest.php similarity index 56% rename from module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php rename to module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/ChangeTrackerServiceTest.php index 94eec7f5cba..adb7636799b 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/ChangeTrackerServiceTest.php @@ -1,11 +1,11 @@ getTable(ChangeTracker::class); + $tracker = $this->getDbService(ChangeTrackerService::class); + + // Ensure that we have a clean slate: + $tracker->deleteRows($core); // Create a new row: $tracker->index($core, 'test1', 1326833170); - $row = $tracker->retrieve($core, 'test1'); + $row = $tracker->getChangeTrackerEntity($core, 'test1'); $this->assertIsObject($row); - $this->assertEmpty($row->deleted); - $this->assertEquals($row->first_indexed, $row->last_indexed); - $this->assertEquals($row->last_record_change, '2012-01-17 20:46:10'); + $this->assertEmpty($row->getDeleted()); + $this->assertEquals($row->getFirstIndexed(), $row->getLastIndexed()); + $this->assertEquals( + $row->getLastRecordChange(), + \DateTime::createFromFormat('Y-m-d H:i:s', '2012-01-17 20:46:10', new \DateTimeZone('UTC')) + ); // Try to index an earlier record version -- changes should be ignored: $tracker->index($core, 'test1', 1326830000); - $row = $tracker->retrieve($core, 'test1'); + $row = $tracker->getChangeTrackerEntity($core, 'test1'); $this->assertIsObject($row); - $this->assertEmpty($row->deleted); - $this->assertEquals($row->first_indexed, $row->last_indexed); - $this->assertEquals($row->last_record_change, '2012-01-17 20:46:10'); - $previousFirstIndexed = $row->first_indexed; + $this->assertEmpty($row->getDeleted()); + $this->assertEquals($row->getFirstIndexed(), $row->getLastIndexed()); + $this->assertEquals( + $row->getLastRecordChange(), + \DateTime::createFromFormat('Y-m-d H:i:s', '2012-01-17 20:46:10', new \DateTimeZone('UTC')) + ); + $previousFirstIndexed = $row->getFirstIndexed(); // Sleep two seconds to be sure timestamps change: sleep(2); // Index a later record version -- this should lead to changes: $tracker->index($core, 'test1', 1326833176); - $row = $tracker->retrieve($core, 'test1'); + $row = $tracker->getChangeTrackerEntity($core, 'test1'); $this->assertIsObject($row); - $this->assertEmpty($row->deleted); - $this->assertTrue( - // use <= in case test runs too fast for values to become unequal: - strtotime($row->first_indexed) <= strtotime($row->last_indexed) + $this->assertEmpty($row->getDeleted()); + $this->assertLessThan($row->getLastIndexed(), $row->getFirstIndexed()); + $this->assertEquals( + $row->getLastRecordChange(), + \DateTime::createFromFormat('Y-m-d H:i:s', '2012-01-17 20:46:16', new \DateTimeZone('UTC')) ); - $this->assertEquals($row->last_record_change, '2012-01-17 20:46:16'); // Make sure the "first indexed" date hasn't changed! - $this->assertEquals($row->first_indexed, $previousFirstIndexed); + $this->assertEquals($row->getFirstIndexed(), $previousFirstIndexed); // Delete the record: $tracker->markDeleted($core, 'test1'); - $row = $tracker->retrieve($core, 'test1'); + $row = $tracker->getChangeTrackerEntity($core, 'test1'); $this->assertIsObject($row); - $this->assertTrue(!empty($row->deleted)); + $this->assertNotEmpty($row->getDeleted()); // Delete a record that hasn't previously been encountered: $tracker->markDeleted($core, 'test2'); - $row = $tracker->retrieve($core, 'test2'); + $row = $tracker->getChangeTrackerEntity($core, 'test2'); $this->assertIsObject($row); - $this->assertTrue(!empty($row->deleted)); + $this->assertTrue(!empty($row->getDeleted())); // Index the previously-deleted record and make sure it undeletes properly: $tracker->index($core, 'test2', 1326833170); - $row = $tracker->retrieve($core, 'test2'); + $row = $tracker->getChangeTrackerEntity($core, 'test2'); $this->assertIsObject($row); - $this->assertEmpty($row->deleted); - $this->assertEquals($row->last_record_change, '2012-01-17 20:46:10'); + $this->assertEmpty($row->getDeleted()); + $this->assertEquals( + $row->getLastRecordChange(), + \DateTime::createFromFormat('Y-m-d H:i:s', '2012-01-17 20:46:10', new \DateTimeZone('UTC')) + ); // Clean up after ourselves: - $tracker->delete(['core' => $core]); + $tracker->deleteRows($core); } } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/DoctrineSchemaValidationTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/DoctrineSchemaValidationTest.php new file mode 100644 index 00000000000..2d20f4c0972 --- /dev/null +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Service/DoctrineSchemaValidationTest.php @@ -0,0 +1,131 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ + +namespace VuFindTest\Db\Service; + +use Doctrine\ORM\Tools\SchemaValidator; + +/** + * Test class to validate the Doctrine schema. + * + * Class must be final due to use of "new static()" by LiveDatabaseTrait. + * + * @category VuFind + * @package Tests + * @author Demian Katz + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +final class DoctrineSchemaValidationTest extends \PHPUnit\Framework\TestCase +{ + use \VuFindTest\Feature\LiveDatabaseTrait; + use \VuFindTest\Feature\LiveDetectionTrait; + + /** + * Standard setup method. + * + * @return void + */ + public function setUp(): void + { + // Give up if we're not running in CI: + if (!$this->continuousIntegrationRunning()) { + $this->markTestSkipped('Continuous integration not running.'); + return; + } + } + + /** + * Test schema validation. + * + * @return void + */ + public function testSchemaValidation(): void + { + $container = $this->getLiveDatabaseContainer(); + // Flush the Doctrine cache to be sure we're validating the latest data: + $cache = $container->get('doctrine.cache.filesystem'); + $cache->flushAll(); + $entityManager = $container->get('doctrine.entitymanager.orm_vufind'); + $platform = $entityManager->getConnection()->getDatabasePlatform()->getName(); + $validator = new SchemaValidator($entityManager); + $schemaList = $validator->getUpdateSchemaList(); + if ($platform === 'postgresql') { + $schemaList = $this->filterIndexRecreation($schemaList); + } + $this->assertEquals([], $validator->validateMapping(), 'Unexpected validation error'); + $this->assertEquals([], $schemaList, 'Unexpected schema updates pending'); + } + + /** + * Filter out warnings related to cross-platform index recreation differences. + * + * MySQL supports column length limits in indexes (e.g., KEY email (email(190))), + * but PostgreSQL doesn't support this syntax natively. This causes Doctrine's + * schema validator to detect differences where MySQL indexes have length + * specifications but PostgreSQL indexes don't, even though both achieve the + * same functional result. + * + * The validator incorrectly reports these as schema mismatches, suggesting + * to DROP and recreate indexes that are functionally identical. This filter + * removes those false positives by excluding any index that appears in both + * DROP and CREATE operations within the same schema diff. + * + * @param array $schemaDiff Array of SQL statements from Doctrine schema comparison + * + * @return array Filtered array with cross-platform index differences removed + */ + public function filterIndexRecreation(array $schemaDiff): array + { + $droppedIndexes = []; + $createdIndexes = []; + + // Collect all dropped and created index names + foreach ($schemaDiff as $statement) { + if (preg_match('/DROP INDEX (\w+)/', $statement, $matches)) { + $droppedIndexes[] = $matches[1]; + } elseif (preg_match('/CREATE (?:UNIQUE )?INDEX (\w+)/', $statement, $matches)) { + $createdIndexes[] = $matches[1]; + } + } + + // Find indexes that are both dropped and created (recreated) + $recreatedIndexes = array_intersect($droppedIndexes, $createdIndexes); + + // Filter out statements for recreated indexes + return array_filter($schemaDiff, function ($statement) use ($recreatedIndexes) { + foreach ($recreatedIndexes as $indexName) { + if (str_contains($statement, $indexName)) { + return false; + } + } + return true; + }); + } +} diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php index ed3bf1e7afc..e747f66cecb 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php @@ -31,7 +31,8 @@ namespace VuFindTest\Mink; -use VuFind\Db\Table\User; +use Doctrine\ORM\EntityManager; +use VuFind\Db\Service\UserService; use function count; @@ -329,8 +330,8 @@ public function testDefaultPickUpLocation(): void $this->submitCatalogLoginForm($page, 'catuser', 'catpass'); // Check the default library and possible values: - $userTable = $this->getTable(User::class); - $this->assertSame('', $userTable->getByUsername('username2')->getHomeLibrary()); + $userService = $this->getDbService(UserService::class); + $this->assertSame('', $userService->getUserByUsername('username2')->getHomeLibrary()); $this->assertEquals( '', $this->findCssAndGetValue($page, '#home_library') @@ -355,9 +356,11 @@ public function testDefaultPickUpLocation(): void $this->clickCss($page, '#profile_form .btn'); $this->waitForPageLoad($page); $this->assertEquals('B', $this->findCssAndGetValue($page, '#home_library')); + $entityManager = $this->getLiveDatabaseContainer()->get(EntityManager::class); + $entityManager->clear(); $this->assertEquals( 'B', - $userTable->getByUsername('username2')->getHomeLibrary() + $userService->getUserByUsername('username2')->getHomeLibrary() ); // Change to "Always ask me": @@ -368,7 +371,8 @@ public function testDefaultPickUpLocation(): void ' ** ', $this->findCssAndGetValue($page, '#home_library') ); - $this->assertNull($userTable->getByUsername('username2')->getHomeLibrary()); + $entityManager->clear(); + $this->assertNull($userService->getUserByUsername('username2')->getHomeLibrary()); // Back to default: $this->findCssAndSetValue($page, '#home_library', ''); @@ -378,7 +382,8 @@ public function testDefaultPickUpLocation(): void '', $this->findCssAndGetValue($page, '#home_library') ); - $this->assertSame('', $userTable->getByUsername('username2')->getHomeLibrary()); + $entityManager->clear(); + $this->assertSame('', $userService->getUserByUsername('username2')->getHomeLibrary()); } /** diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/FeedbackServiceTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/FeedbackServiceTest.php new file mode 100644 index 00000000000..c5594e57af6 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/FeedbackServiceTest.php @@ -0,0 +1,164 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ + +namespace VuFindTest\Db\Service; + +use Doctrine\ORM\Configuration; +use VuFind\Db\Entity\Feedback; +use VuFind\Db\Entity\FeedbackEntityInterface; +use VuFind\Db\PersistenceManager; +use VuFind\Db\Service\FeedbackService; + +/** + * FeedbackService Test Class + * + * @category VuFind + * @package Tests + * @author Sudharma Kellampalli + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class FeedbackServiceTest extends \PHPUnit\Framework\TestCase +{ + /** + * Test creating a feedback entity. + * + * @return void + */ + public function testCreateEntity(): void + { + $configuredService = $this->getConfiguredFeedbackService(); + $configuredService['entityPluginManager']->expects($this->once())->method('get') + ->with($this->equalTo(FeedbackEntityInterface::class)) + ->willReturn(new Feedback()); + + $this->assertInstanceOf( + Feedback::class, + $configuredService['feedbackService']->createEntity() + ); + } + + /** + * Test getting column values. + * + * @return void + */ + public function testGetColumn(): void + { + $mocks = $this->getConfiguredFeedbackService(); + $entityManager = $mocks['entityManager']; + $feedbackService = $mocks['feedbackService']; + $queryStmt = "SELECT f.id, f.status FROM VuFind\Db\Entity\FeedbackEntityInterface f " + . 'ORDER BY f.status'; + $query = $this->getMockBuilder(\Doctrine\ORM\AbstractQuery::class) + ->disableOriginalConstructor() + ->onlyMethods(['getResult']) + ->getMockForAbstractClass(); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + $query->expects($this->once())->method('getResult') + ->willReturn([]); + $feedbackService->getColumn('status'); + } + + /** + * Test delete based on id. + * + * @return void + */ + public function testDeleteByIdArray(): void + { + $mocks = $this->getConfiguredFeedbackService(); + $entityManager = $mocks['entityManager']; + $feedbackService = $mocks['feedbackService']; + $queryStmt = "DELETE FROM VuFind\Db\Entity\FeedbackEntityInterface fb WHERE fb.id IN (:ids)"; + + $query = $this->getMockBuilder(\Doctrine\ORM\AbstractQuery::class) + ->disableOriginalConstructor() + ->onlyMethods(['execute', 'setParameters']) + ->getMockForAbstractClass(); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + $query->expects($this->once())->method('execute'); + $query->expects($this->once())->method('setParameters') + ->with(['ids' => [1,2]]) + ->willReturn($query); + $feedbackService->deleteByIdArray([1, 2]); + } + + /** + * Test getting feedback based on filters. + * + * @return void + */ + public function testGetFeedbackPaginator(): void + { + $mocks = $this->getConfiguredFeedbackService(); + $entityManager = $mocks['entityManager']; + $feedbackService = $mocks['feedbackService']; + $queryStmt = "SELECT f AS feedback_entity FROM VuFind\Db\Entity\FeedbackEntityInterface f " + . 'WHERE f.formName = :formName AND f.siteUrl = :siteUrl AND ' + . 'f.status = :status ORDER BY f.created DESC'; + + $entityManager->method('getConfiguration')->willReturn($this->createMock(Configuration::class)); + $query = $this->getMockBuilder(\Doctrine\ORM\Query::class) + ->setConstructorArgs([$entityManager]) + ->onlyMethods(['setParameters', 'setFirstResult', 'setMaxResults']) + ->getMock(); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + + $query->expects($this->once())->method('setParameters') + ->with( + ['formName' => 'foo', + 'siteUrl' => 'bar', + 'status' => 'closed'] + ) + ->willReturn($query); + + $feedbackService->getFeedbackPaginator('foo', 'bar', 'closed'); + } + + /** + * Get a configured FeedbackService object. + * + * @return array + */ + protected function getConfiguredFeedbackService() + { + $entityManager = $this->createMock(\Doctrine\ORM\EntityManager::class); + $entityPluginManager = $this->createMock(\VuFind\Db\Entity\PluginManager::class); + $persistenceManager = $this->createMock(PersistenceManager::class); + $feedbackService = new FeedbackService($entityManager, $entityPluginManager, $persistenceManager); + return compact('entityManager', 'entityPluginManager', 'feedbackService'); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/OaiResumptionServiceTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/OaiResumptionServiceTest.php index 156eb3134e8..2446020945f 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/OaiResumptionServiceTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/OaiResumptionServiceTest.php @@ -22,19 +22,23 @@ * * @category VuFind * @package Tests + * @author Sudharma Kellampalli * @author Juha Luoma * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:testing:unit_tests Wiki */ -namespace VuFindTest\Service; +namespace VuFindTest\Db\Service; +use Doctrine\ORM\EntityManager; use Exception; use Generator; -use Laminas\Db\ResultSet\AbstractResultSet; +use PHPUnit\Framework\MockObject\MockObject; +use VuFind\Db\Entity\OaiResumption; use VuFind\Db\Entity\OaiResumptionEntityInterface; +use VuFind\Db\Entity\PluginManager; +use VuFind\Db\PersistenceManager; use VuFind\Db\Service\OaiResumptionService; -use VuFindTest\Container\MockContainer; use function count; use function intval; @@ -44,27 +48,156 @@ * * @category VuFind * @package Tests + * @author Sudharma Kellampalli * @author Juha Luoma * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License * @link https://vufind.org/wiki/development:testing:unit_tests Wiki */ class OaiResumptionServiceTest extends \PHPUnit\Framework\TestCase { + use \VuFindTest\Feature\ReflectionTrait; + + /** + * OaiResumption service object to test. + * + * @param MockObject&EntityManager $entityManager Mock entity manager object + * @param MockObject&PluginManager $pluginManager Mock plugin manager object + * @param ?OaiResumptionEntityInterface $oaiResumption Mock OaiResumption entity object + * + * @return MockObject + */ + protected function getService( + MockObject&EntityManager $entityManager, + MockObject&PluginManager $pluginManager, + ?OaiResumptionEntityInterface $oaiResumption = null, + ): MockObject&OaiResumptionService { + $persistenceManager = $this->createMock(PersistenceManager::class); + $serviceMock = $this->getMockBuilder(OaiResumptionService::class) + ->onlyMethods(['createEntity']) + ->setConstructorArgs([$entityManager, $pluginManager, $persistenceManager]) + ->getMock(); + if ($oaiResumption) { + $serviceMock->expects($this->once())->method('createEntity') + ->willReturn($oaiResumption); + } + return $serviceMock; + } + + /** + * Mock entity plugin manager. + * + * @param bool $setExpectation Flag to set the method expectations. + * + * @return MockObject&PluginManager + */ + protected function getPluginManager(bool $setExpectation = false): MockObject&PluginManager + { + $pluginManager = $this->createMock(PluginManager::class); + if ($setExpectation) { + $pluginManager->expects($this->once())->method('get') + ->with($this->equalTo(OaiResumptionEntityInterface::class)) + ->willReturn(new OaiResumption()); + } + return $pluginManager; + } + + /** + * Mock entity manager. + * + * @param int $count Expectation count + * + * @return MockObject&EntityManager + */ + protected function getEntityManager(int $count = 0): MockObject&EntityManager + { + $entityManager = $this->createMock(EntityManager::class); + $entityManager->expects($this->exactly($count))->method('persist'); + $entityManager->expects($this->exactly($count))->method('flush'); + return $entityManager; + } + + /** + * Test removing all expired tokens from the database. + * + * @return void + */ + public function testRemoveExpired(): void + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(); + $resumptionService = $this->getService($entityManager, $pluginManager); + $queryStmt = "DELETE FROM VuFind\Db\Entity\OaiResumptionEntityInterface O WHERE O.expires <= :now"; + + $query = $this->createMock(\Doctrine\ORM\AbstractQuery::class); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + $query->expects($this->once())->method('execute'); + $query->expects($this->once())->method('setParameters') + ->with($this->anything()) + ->willReturn($query); + $resumptionService->removeExpired(); + } + /** - * Mock container + * Test retrieving a row from the database based on primary key. * - * @var MockContainer + * @return void */ - protected MockContainer $container; + public function testFindToken(): void + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(); + $resumptionService = $this->getService($entityManager, $pluginManager); + $queryStmt = "SELECT O FROM VuFind\Db\Entity\OaiResumptionEntityInterface O WHERE O.id = :id"; + + $query = $this->createMock(\Doctrine\ORM\AbstractQuery::class); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + $oaiResumption = $this->createMock(\VuFind\Db\Entity\OaiResumption::class); + $query->expects($this->once())->method('getOneOrNullResult') + ->willReturn($oaiResumption); + $query->expects($this->once())->method('setParameters') + ->with(['id' => 'foo']) + ->willReturn($query); + $this->assertEquals($oaiResumption, $resumptionService->findToken('foo')); + } /** - * Setup test environment. Always call parent method here. + * Data provide for testEncodeParams() + * + * @return array + */ + public static function encodeParamsProvider(): array + { + // The expected result is encoded in the test below; both data sets represent the + // same values, but in different orders. We want to be sure the result is the same + // regardless of order. + return [ + 'sorted keys' => [['cursor' => 20, 'cursorMark' => 100, 'foo' => 'bar']], + 'unsorted keys' => [['foo' => 'bar', 'cursorMark' => 100, 'cursor' => 20]], + ]; + } + + /** + * Test encoding parameters. + * + * @param array $params Parameters to encode. * * @return void + * + * @dataProvider encodeParamsProvider */ - public function setup(): void + public function testEncodeParams(array $params): void { - $this->container = new MockContainer($this); + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(); + $resumptionService = $this->getService($entityManager, $pluginManager); + $this->assertEquals( + 'cursor=20&cursorMark=100&foo=bar', + $this->callMethod($resumptionService, 'encodeParams', [$params]) + ); } /** @@ -138,7 +271,8 @@ public function testDuplicates(array $token, array $randomTokenSequence, string $this->expectExceptionMessage($error); } $previousToken = ''; - $row = $this->container->createMock(\VuFind\Db\Row\OaiResumption::class, ['save', 'getToken', 'setToken']); + $container = new \VuFindTest\Container\MockContainer($this); + $row = $container->createMock(OaiResumption::class, ['getToken', 'setToken']); $row->expects($this->any())->method('getToken')->willReturnCallback( function () use (&$previousToken) { return $previousToken; @@ -150,9 +284,9 @@ function ($t) use (&$previousToken, $row) { return $row; } ); - $oaiResumptionService = $this->container->createMock( + $oaiResumptionService = $container->createMock( OaiResumptionService::class, - ['createRandomToken', 'createEntity'] + ['createRandomToken', 'createEntity', 'persistEntity'] ); $oaiResumptionService->expects($this->any())->method('createRandomToken')->willReturnCallback( function () use (&$randomTokenSequence, $row) { @@ -227,45 +361,41 @@ public static function getTestTokenRetrieval(): Generator */ public function testTokenRetrieval(string $token, ?string $expectedParams): void { - $mockRow = $this->container->createMock(OaiResumptionEntityInterface::class, []); + $container = new \VuFindTest\Container\MockContainer($this); + $mockRow = $container->createMock(OaiResumptionEntityInterface::class, []); $mockDb = []; foreach ($this->mockEntities as $entity) { $rowClone = clone $mockRow; $rowClone->expects($this->any())->method('getId')->willReturn($entity['id']); $rowClone->setExpiry(\DateTime::createFromFormat('U', $entity['expires'])); $rowClone->expects($this->any())->method('getResumptionParameters')->willReturn($entity['params']); - if ($entity['token']) { - $rowClone->expects($this->any())->method('getToken')->willReturn($entity['token']); - } + $rowClone->expects($this->any())->method('getToken')->willReturn($entity['token']); $mockDb[] = $rowClone; } - $mockTable = $this->container->createMock(\VuFind\Db\Table\OaiResumption::class, ['select']); + $mockService = $container->createMock(OaiResumptionService::class, ['findWithToken', 'findWithLegacyIdToken']); - $mockTable->expects($this->any())->method('select')->willReturnCallback(function ($select) use ($mockDb) { - $result = []; + $lookupFunction = function ($select) use ($mockDb) { foreach ($mockDb as $entry) { - if (!empty($select['id'])) { - if ($entry->getId() === intval($select['id']) && $entry->getToken() === $select['token']) { - $result[] = $entry; - } - continue; + if (!empty($select['id']) && $entry->getId() === intval($select['id'])) { + return $entry; } - if (!empty($select['token'])) { - if ($entry->getToken() === $select['token']) { - $result[] = $entry; - } + if (!empty($select['token']) && $entry->getToken() === $select['token']) { + return $entry; } } - $mockResultSet = $this->container->createMock(AbstractResultSet::class, ['current']); - $mockResultSet->expects($this->any())->method('current')->willReturn($result[0] ?? null); - return $mockResultSet; - }); - $oaiResumptionService = $this->container->createMock( - OaiResumptionService::class, - ['getDbTable'] + return null; + }; + $mockService->expects($this->any())->method('findWithToken')->willReturnCallback( + function ($token) use ($lookupFunction) { + return $lookupFunction(compact('token')); + } + ); + $mockService->expects($this->any())->method('findWithLegacyIdToken')->willReturnCallback( + function ($id) use ($lookupFunction) { + return $lookupFunction(compact('id')); + } ); - $oaiResumptionService->expects($this->any())->method('getDbTable')->willReturn($mockTable); - $token = $oaiResumptionService->findWithTokenOrLegacyIdToken($token); + $token = $mockService->findWithTokenOrLegacyIdToken($token); $this->assertEquals($expectedParams, $expectedParams ? $token->getResumptionParameters() : null); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/SessionServiceTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/SessionServiceTest.php new file mode 100644 index 00000000000..bce1cc6b104 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/SessionServiceTest.php @@ -0,0 +1,345 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ + +namespace VuFindTest\Db\Service; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\QueryBuilder; +use PHPUnit\Framework\MockObject\MockObject; +use VuFind\Db\Entity\PluginManager; +use VuFind\Db\Entity\Session; +use VuFind\Db\Entity\SessionEntityInterface; +use VuFind\Db\PersistenceManager; +use VuFind\Db\Service\SessionService; + +/** + * SessionService Test Class + * + * @category VuFind + * @package Tests + * @author Sudharma Kellampalli + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:testing:unit_tests Wiki + */ +class SessionServiceTest extends \PHPUnit\Framework\TestCase +{ + /** + * Mock entity plugin manager. + * + * @param bool $setExpectation Flag to set the method expectations. + * + * @return PluginManager&MockObject + */ + protected function getPluginManager($setExpectation = false): PluginManager&MockObject + { + $pluginManager = $this->createMock(PluginManager::class); + if ($setExpectation) { + $pluginManager->expects($this->once())->method('get') + ->with(SessionEntityInterface::class) + ->willReturn(new Session()); + } + return $pluginManager; + } + + /** + * Mock persistence manager. + * + * @param int $count Expectation count + * + * @return PersistenceManager&MockObject + */ + protected function getPersistenceManager(int $count = 0): PersistenceManager&MockObject + { + $entityManager = $this->createMock(PersistenceManager::class); + $entityManager->expects($this->exactly($count))->method('persistEntity'); + return $entityManager; + } + + /** + * Mock queryBuilder + * + * @param string $parameter Input query parameter + * @param array $result Expected return value of getResult method. + * + * @return QueryBuilder&MockObject + */ + protected function getQueryBuilder(string $parameter, array $result): QueryBuilder&MockObject + { + $queryBuilder = $this->createMock(QueryBuilder::class); + $queryBuilder->expects($this->once())->method('select') + ->with('s') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('from') + ->with(SessionEntityInterface::class, 's') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('where') + ->with('s.sessionId = :sid') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('setParameter') + ->with('sid', $parameter) + ->willReturn($queryBuilder); + $query = $this->getMockBuilder(\Doctrine\ORM\AbstractQuery::class) + ->disableOriginalConstructor() + ->onlyMethods(['getResult']) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('getResult') + ->willReturn($result); + $queryBuilder->expects($this->once())->method('getQuery') + ->willReturn($query); + return $queryBuilder; + } + + /** + * Session service object to test. + * + * @param EntityManager $entityManager Mock entity manager object + * @param PluginManager $pluginManager Mock plugin manager object + * @param PersistenceManager $persistenceManager Persistence manager object + * @param ?SessionEntityInterface $session Mock session entity object + * + * @return SessionService&MockObject + */ + protected function getService( + EntityManager $entityManager, + PluginManager $pluginManager, + PersistenceManager $persistenceManager, + ?SessionEntityInterface $session = null, + ): SessionService&MockObject { + $serviceMock = $this->getMockBuilder(SessionService::class) + ->onlyMethods(['createEntity']) + ->setConstructorArgs([$entityManager, $pluginManager, $persistenceManager]) + ->getMock(); + if ($session) { + $serviceMock->expects($this->once())->method('createEntity') + ->willReturn($session); + } + return $serviceMock; + } + + /** + * Test retrieving an session object from database. + * + * @return void + */ + public function testGetSessionById(): void + { + $session = $this->createMock(Session::class); + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(); + $queryBuilder = $this->getQueryBuilder('1', [$session]); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $this->assertEquals($session, $service->getSessionById('1', false)); + } + + /** + * Test the case where a session is not found and creating a new session + * is not required. + * + * @return void + */ + public function testSessionNotFound(): void + { + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(); + $queryBuilder = $this->getQueryBuilder('1', []); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $this->assertNull($service->getSessionById('1', false)); + } + + /** + * Test creating a new session if no existing session is found. + * + * @return void + */ + public function testCreatingSession(): void + { + $session = $this->createMock(Session::class); + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(1); + $queryBuilder = $this->getQueryBuilder('1', []); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $session->expects($this->once())->method('setSessionId') + ->with($this->equalTo('1')) + ->willReturn($session); + $session->expects($this->once())->method('setCreated') + ->with($this->anything()) + ->willReturn($session); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager, $session); + $this->assertEquals($session, $service->getSessionById('1', true)); + } + + /** + * Test reading session data. + * + * @return void + */ + public function testReadSession(): void + { + $session = $this->createMock(Session::class); + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(1); + $queryBuilder = $this->getQueryBuilder('1', [$session]); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $session->expects($this->once())->method('getLastUsed') + ->willReturn(time() - 1000); + $session->expects($this->once())->method('setLastUsed') + ->with($this->anything()); + $session->expects($this->once())->method('getData') + ->willReturn('foo'); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $this->assertEquals('foo', $service->readSession('1', 10000000)); + } + + /** + * Test reading expired session data. + * + * @return void + */ + public function testReadingExpiredSession(): void + { + $this->expectException(\VuFind\Exception\SessionExpired::class); + $this->expectExceptionMessage('Session expired!'); + $session = $this->createMock(Session::class); + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(); + $queryBuilder = $this->getQueryBuilder('1', [$session]); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $session->expects($this->once())->method('getLastUsed') + ->willReturn(time() - 1000); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $service->readSession('1', 100); + } + + /** + * Test storing session data. + * + * @return void + */ + public function testWriteSession(): void + { + $session = $this->createMock(Session::class); + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(1); + $queryBuilder = $this->getQueryBuilder('1', [$session]); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $session->expects($this->once())->method('setLastUsed') + ->with($this->anything()) + ->willReturn($session); + $session->expects($this->once())->method('setData') + ->with('foo') + ->willReturn($session); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $this->assertEquals(true, $service->WriteSession('1', 'foo')); + } + + /** + * Test destroying the session. + * + * @return void + */ + public function testDestroySession(): void + { + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(); + $queryBuilder = $this->createMock(\Doctrine\ORM\QueryBuilder::class); + $queryBuilder->expects($this->once())->method('delete') + ->with(SessionEntityInterface::class, 's') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('where') + ->with('s.sessionId = :sid') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('setParameter') + ->with('sid', 1) + ->willReturn($queryBuilder); + $query = $this->getMockBuilder(\Doctrine\ORM\AbstractQuery::class) + ->disableOriginalConstructor() + ->onlyMethods(['execute']) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('execute') + ->willReturn($this->anything()); + $queryBuilder->expects($this->once())->method('getQuery') + ->willReturn($query); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $service->destroySession('1'); + } + + /** + * Test destroying the expired sessions. + * + * @return void + */ + public function testGarbageCollect(): void + { + $entityManager = $this->createMock(EntityManager::class); + $pluginManager = $this->getPluginManager(); + $persistenceManager = $this->getPersistenceManager(); + $countQuery = $this->createMock(\Doctrine\ORM\AbstractQuery::class); + $countQuery->method('getSingleScalarResult')->willReturn(5); + $countQuery->expects($this->once())->method('setParameter') + ->with('used', $this->equalToWithDelta(time() - 10000, 1)); + $countDql = "SELECT COUNT(s) FROM VuFind\Db\Entity\SessionEntityInterface s WHERE s.lastUsed < :used"; + $entityManager->expects($this->once())->method('createQuery')->with($countDql)->willReturn($countQuery); + $queryBuilder = $this->createMock(\Doctrine\ORM\QueryBuilder::class); + $queryBuilder->expects($this->once())->method('delete') + ->with(SessionEntityInterface::class, 's') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('where') + ->with('s.lastUsed < :used') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('setParameter') + ->with('used', $this->equalToWithDelta(time() - 10000, 1)) + ->willReturn($queryBuilder); + $deleteQuery = $this->createMock(\Doctrine\ORM\AbstractQuery::class); + $deleteQuery->expects($this->once())->method('execute') + ->willReturn($this->anything()); + $queryBuilder->expects($this->once())->method('getQuery') + ->willReturn($deleteQuery); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager, $persistenceManager); + $this->assertEquals(5, $service->garbageCollect(10000)); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php deleted file mode 100644 index 476569e59fb..00000000000 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:testing:unit_tests Wiki - */ - -namespace VuFindTest\Db\Table; - -use VuFind\Db\Table\PluginManager; - -/** - * DB Table Plugin Manager Test Class - * - * @category VuFind - * @package Tests - * @author Demian Katz - * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org/wiki/development:testing:unit_tests Wiki - */ -class PluginManagerTest extends \PHPUnit\Framework\TestCase -{ - use \VuFindTest\Feature\ReflectionTrait; - - /** - * Test results. - * - * @return void - */ - public function testShareByDefault() - { - $pm = new PluginManager(new \VuFindTest\Container\MockContainer($this)); - $this->assertTrue($this->getProperty($pm, 'sharedByDefault')); - } - - /** - * Test expected interface. - * - * @return void - */ - public function testExpectedInterface() - { - $this->expectException(\Laminas\ServiceManager\Exception\InvalidServiceException::class); - $this->expectExceptionMessage('Plugin ArrayObject does not belong to VuFind\\Db\\Table\\Gateway'); - - $pm = new PluginManager(new \VuFindTest\Container\MockContainer($this)); - $pm->validate(new \ArrayObject()); - } -} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php index 35c60f5d1ef..fa9dd91e4fb 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php @@ -34,6 +34,7 @@ use VuFind\Db\Entity\UserListEntityInterface; use VuFind\Db\Service\ResourceServiceInterface; use VuFind\Db\Service\ResourceTagsService; +use VuFind\Db\Service\UserListService; use VuFind\Db\Service\UserListServiceInterface; use VuFind\Db\Service\UserResourceServiceInterface; use VuFind\Db\Service\UserServiceInterface; @@ -111,7 +112,7 @@ public function testNewListIsPopulatedCorrectly() $newList = $this->createMock(UserListEntityInterface::class); $newList->expects($this->once())->method('setCreated')->willReturn($newList); $newList->expects($this->once())->method('setUser')->with($user)->willReturn($newList); - $listService = $this->createMock(UserListServiceInterface::class); + $listService = $this->createMock(UserListService::class); $listService->expects($this->once())->method('createEntity')->willReturn($newList); $service = $this->getFavoritesService($listService); $service->createListForUser($user); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Form/Handler/DatabaseTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Form/Handler/DatabaseTest.php index 42cef568ba2..54a2d44e186 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Form/Handler/DatabaseTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Form/Handler/DatabaseTest.php @@ -34,6 +34,7 @@ use VuFind\Db\Entity\FeedbackEntityInterface; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Service\FeedbackServiceInterface; +use VuFind\Db\Service\UserService; use VuFind\Form\Form; use VuFind\Form\Handler\Database; @@ -80,7 +81,8 @@ public function testSuccessWithUser(): void $feedbackService = $this->createMock(FeedbackServiceInterface::class); $feedbackService->expects($this->once())->method('createEntity')->willReturn($feedback); $feedbackService->expects($this->once())->method('persistEntity')->with($feedback); - $handler = new Database($feedbackService, 'http://foo'); + $userService = $this->createMock(UserService::class); + $handler = new Database($feedbackService, $userService, 'http://foo'); $form = $this->createMock(Form::class); $form->expects($this->once())->method('mapRequestParamsToFieldValues')->willReturn([]); $form->expects($this->once())->method('getFormId')->willReturn('formy-mcformface'); @@ -101,7 +103,8 @@ public function testSuccessWithoutUser(): void $feedbackService = $this->createMock(FeedbackServiceInterface::class); $feedbackService->expects($this->once())->method('createEntity')->willReturn($feedback); $feedbackService->expects($this->once())->method('persistEntity')->with($feedback); - $handler = new Database($feedbackService, 'http://foo'); + $userService = $this->createMock(UserService::class); + $handler = new Database($feedbackService, $userService, 'http://foo'); $form = $this->createMock(Form::class); $form->expects($this->once())->method('mapRequestParamsToFieldValues')->willReturn([]); $form->expects($this->once())->method('getFormId')->willReturn('formy-mcformface'); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AbstractTokenRepositoryTestCase.php b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AbstractTokenRepositoryTestCase.php index 749f16f4e6a..598daa9841f 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AbstractTokenRepositoryTestCase.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AbstractTokenRepositoryTestCase.php @@ -30,18 +30,19 @@ namespace VuFindTest\OAuth2\Repository; use PHPUnit\Framework\MockObject\MockObject; -use VuFind\Db\Row\AccessToken as AccessTokenRow; -use VuFind\Db\Row\User as UserRow; +use VuFind\Db\Entity\AccessToken; +use VuFind\Db\Entity\AccessTokenEntityInterface; +use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Service\AccessTokenService; use VuFind\Db\Service\AccessTokenServiceInterface; use VuFind\Db\Service\UserServiceInterface; -use VuFind\Db\Table\AccessToken; -use VuFind\Db\Table\User; use VuFind\OAuth2\Entity\ClientEntity; use VuFind\OAuth2\Repository\AccessTokenRepository; use VuFind\OAuth2\Repository\AuthCodeRepository; use VuFind\OAuth2\Repository\RefreshTokenRepository; +use function count; + /** * Abstract base class for OAuth2 token repository tests. * @@ -55,6 +56,8 @@ abstract class AbstractTokenRepositoryTestCase extends \PHPUnit\Framework\TestCa { protected $accessTokenTable = []; + public $entityManager = null; + /** * Create AccessTokenRepository with mocks. * @@ -108,121 +111,105 @@ protected function getOAuth2Config(): array } /** - * Create AccessToken table + * Mock entity manager. * - * @return MockObject&AccessToken + * @return MockObject */ - protected function getMockAccessTokenTable(): AccessToken + protected function getEntityManager() { - $getByIdAndTypeCallback = function ( - string $id, - string $type, - bool $create - ): ?AccessTokenRow { - foreach ($this->accessTokenTable as $row) { - if ( - $id === $row['id'] - && $type === $row['type'] - ) { - return $this->createAccessTokenRow($row); - } - } - $revoked = false; - $user_id = null; - return $create - ? $this->createAccessTokenRow( - compact('id', 'type', 'revoked', 'user_id') - ) : null; - }; - - $accessTokenTable = $this->getMockBuilder(AccessToken::class) + $entityManager = $this->getMockBuilder(\Doctrine\ORM\EntityManager::class) ->disableOriginalConstructor() - ->onlyMethods(['getByIdAndType']) + ->onlyMethods(['createQuery','persist','flush']) ->getMock(); - $accessTokenTable->expects($this->any()) - ->method('getByIdAndType') - ->willReturnCallback($getByIdAndTypeCallback); - - return $accessTokenTable; + $query = $this->createMock(\Doctrine\ORM\Query::class); + $entityManager->expects($this->any())->method('createQuery')->willReturn($query); + $entityManager->expects($this->any())->method('persist'); + $entityManager->expects($this->any())->method('flush'); + return $entityManager; } /** - * Create User table + * Mock entity plugin manager. + * + * @param bool $setExpectation Flag to set the method expectations. * - * @return MockObject&User + * @return MockObject */ - protected function getMockUserTable(): User + protected function getPluginManager($setExpectation = false) { - $getByIdCallback = function ( - $id - ): ?UserRow { - $username = 'test'; - return $this->createUserRow(compact('id', 'username')); - }; - - $accessTokenTable = $this->getMockBuilder(User::class) - ->disableOriginalConstructor() - ->onlyMethods(['getById']) - ->getMock(); - $accessTokenTable->expects($this->any()) - ->method('getById') - ->willReturnCallback($getByIdCallback); - - return $accessTokenTable; + $pluginManager = $this->createMock(\VuFind\Db\Entity\PluginManager::class); + if ($setExpectation) { + $pluginManager->expects($this->any())->method('get') + ->with($this->equalTo(AccessToken::class)) + ->willReturn(new AccessToken()); + } + return $pluginManager; } /** - * Create AccessToken row + * Create a mock AccessTokenEntity from an array of values. * - * @param array $data Row data + * @param array $fields Field values * - * @return MockObject&AccessTokenRow + * @return AccessTokenEntityInterface&MockObject */ - protected function createAccessTokenRow(array $data): AccessTokenRow + protected function createAccessTokenEntity(array $fields): AccessTokenEntityInterface&MockObject { - $result = $this->getMockBuilder(AccessTokenRow::class) - ->disableOriginalConstructor() - ->onlyMethods(['initialize', 'save']) - ->getMock(); - $result->populate($data); - - $save = function () use ($result) { - $data = $result->toArray(); - foreach ($this->accessTokenTable as &$row) { - if ( - $data['id'] === $row['id'] - && $data['type'] === $row['type'] - ) { - $row = $data; - return 1; - } + $i = $this->findAccessTokenTableRow($fields); + if ($i === null) { + $i = count($this->accessTokenTable); + $this->accessTokenTable[] = $fields; + } + $mock = $this->createMock(AccessTokenEntityInterface::class); + $mock->method('getId')->willReturnCallback(fn () => (string)$this->accessTokenTable[$i]['id']); + $mock->method('getType')->willReturnCallback(fn () => $this->accessTokenTable[$i]['type'] ?? null); + $mock->method('getUser')->willReturnCallback(function () use ($i) { + $userId = $this->accessTokenTable[$i]['user_id'] ?? null; + if ($userId) { + return $this->getMockUserService()->getUserByField('id', $userId); } - $this->accessTokenTable[] = $data; - return 1; - }; - - $result->expects($this->any()) - ->method('save') - ->willReturnCallback($save); - - return $result; + return null; + }); + $mock->method('getData')->willReturnCallback(fn () => $this->accessTokenTable[$i]['data'] ?? null); + $mock->method('isRevoked')->willReturnCallback(fn () => $this->accessTokenTable[$i]['revoked'] ?? false); + $mock->method('setData')->willReturnCallback(function ($data) use ($i, $mock) { + $this->accessTokenTable[$i]['data'] = $data; + return $mock; + }); + $mock->method('setType')->willReturnCallback(function ($type) use ($i, $mock) { + $this->accessTokenTable[$i]['type'] = $type; + return $mock; + }); + $mock->method('setUser')->willReturnCallback(function ($user) use ($i, $mock) { + $this->accessTokenTable[$i]['user_id'] = $user?->getId(); + return $mock; + }); + $mock->method('setRevoked')->willReturnCallback(function ($revoked) use ($i, $mock) { + $this->accessTokenTable[$i]['revoked'] = $revoked; + return $mock; + }); + return $mock; } /** - * Create User row + * Find a row matching the provided data in our virtual data table; return null + * if no match is found. * - * @param array $data Row data + * @param array $data Data to match * - * @return MockObject&UserRow + * @return ?int */ - protected function createUserRow(array $data): UserRow + protected function findAccessTokenTableRow(array $data): ?int { - $result = $this->getMockBuilder(UserRow::class) - ->disableOriginalConstructor() - ->onlyMethods(['initialize']) - ->getMock(); - $result->populate($data); - return $result; + foreach ($this->accessTokenTable as $i => $row) { + if ( + $data['id'] === $row['id'] + && $data['type'] === $row['type'] + ) { + return $i; + } + } + return null; } /** @@ -230,49 +217,124 @@ protected function createUserRow(array $data): UserRow * * @return MockObject&AccessTokenServiceInterface */ - protected function getMockAccessTokenService(): AccessTokenServiceInterface + protected function getMockAccessTokenService(): AccessTokenServiceInterface&MockObject { - $accessTokenTable = $this->getMockAccessTokenTable(); $accessTokenService = $this->getMockBuilder(AccessTokenService::class) ->disableOriginalConstructor() - ->onlyMethods( - [ - 'getByIdAndType', - 'getNonce', - 'storeNonce', - ] - ) + ->onlyMethods(['getByIdAndType', 'persistEntity', 'storeNonce', 'getNonce']) ->getMock(); + + $getByIdAndTypeCallback = function ( + string $id, + string $type, + bool $create + ): ?AccessTokenEntityInterface { + foreach ($this->accessTokenTable as $row) { + if ( + $id === $row['id'] + && $type === $row['type'] + ) { + return $this->createAccessTokenEntity($row); + } + } + $revoked = false; + $user_id = null; + return $create + ? $this->createAccessTokenEntity( + compact('id', 'type', 'revoked', 'user_id') + ) : null; + }; $accessTokenService->expects($this->any()) ->method('getByIdAndType') - ->willReturnCallback([$accessTokenTable, 'getByIdAndType']); + ->willReturnCallback($getByIdAndTypeCallback); + $persistEntityCallback = function (AccessTokenEntityInterface $entity): void { + $data = [ + 'id' => $entity->getId(), + 'type' => $entity->getType(), + 'revoked' => $entity->isRevoked(), + 'data' => $entity->getData(), + 'user_id' => $entity->getUser()?->getId(), + ]; + if (null !== ($i = $this->findAccessTokenTableRow($data))) { + $this->accessTokenTable[$i] = $data; + return; + } + $this->accessTokenTable[] = $data; + }; + $accessTokenService->expects($this->any()) + ->method('persistEntity') + ->willReturnCallback($persistEntityCallback); + + $getNonceCallback = function (int $userId): ?string { + foreach ($this->accessTokenTable as $row) { + if ($userId === $row['user_id']) { + return $row['data']; + } + } + return null; + }; $accessTokenService->expects($this->any()) ->method('getNonce') - ->willReturnCallback([$accessTokenTable, 'getNonce']); + ->willReturnCallback($getNonceCallback); + + $storeNonceCallback = function (int $userId, ?string $nonce): void { + $data = [ + 'id' => 2, + 'type' => 'oauth2_access_token', + 'revoked' => false, + 'data' => $nonce, + 'user_id' => $userId, + ]; + if (null !== ($i = $this->findAccessTokenTableRow($data))) { + $this->accessTokenTable[$i] = $data; + return; + } + $this->accessTokenTable[] = $data; + }; $accessTokenService->expects($this->any()) ->method('storeNonce') - ->willReturnCallback([$accessTokenTable, 'storeNonce']); + ->willReturnCallback($storeNonceCallback); return $accessTokenService; } + /** + * Create User entity mock. + * + * @param ?int $id User ID + * @param string $username User's name + * + * @return MockObject&UserEntityInterface&null + */ + protected function createMockUserEntity(?int $id, string $username): UserEntityInterface&MockObject + { + $mockUser = $this->createMock(UserEntityInterface::class); + if ($id !== null) { + $mockUser->expects($this->any()) + ->method('getId') + ->willReturn($id); + $mockUser->expects($this->any()) + ->method('getUsername') + ->willReturn($username); + } + return $mockUser; + } + /** * Create User service * * @return MockObject&UserServiceInterface */ - protected function getMockUserService(): UserServiceInterface + protected function getMockUserService(): UserServiceInterface&MockObject { - $userTable = $this->getMockUserTable(); - $userService = $this->createMock(UserServiceInterface::class); - $userService->expects($this->any()) + $mockUserService = $this->createMock(UserServiceInterface::class); + $mockUserService->expects($this->any()) ->method('getUserByField') - ->willReturnCallback( - function ($fieldName, $fieldValue) use ($userTable) { - $this->assertEquals('id', $fieldName); - return $userTable->getById($fieldValue); - } - ); - return $userService; + ->willReturnCallback(function (string $fieldName, $fieldValue) { + $this->assertEquals('id', $fieldName); + return $this->createMockUserEntity($fieldValue, 'test'); + }); + + return $mockUserService; } /** diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AccessTokenRepositoryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AccessTokenRepositoryTest.php index 4879b7f8f18..b2885990ab2 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AccessTokenRepositoryTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/AccessTokenRepositoryTest.php @@ -59,7 +59,6 @@ public function testAccessTokenRepository(): void $tokenId = $this->createTokenId(); $token->setIdentifier($tokenId); $token->setExpiryDateTime($this->createExpiryDateTime()); - $repo->persistNewAccessToken($token); $this->assertEquals( [ @@ -68,7 +67,7 @@ public function testAccessTokenRepository(): void 'type' => 'oauth2_access_token', 'revoked' => false, 'data' => json_encode($token), - 'user_id' => '1', + 'user_id' => 1, ], ], $this->accessTokenTable diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/IdentityRepositoryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/IdentityRepositoryTest.php index 5cf09be8f23..c10ed6bca56 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/IdentityRepositoryTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/OAuth2/Repository/IdentityRepositoryTest.php @@ -35,7 +35,6 @@ use PHPUnit\Framework\MockObject\MockObject; use VuFind\Auth\ILSAuthenticator; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Row\User; use VuFind\Db\Service\UserServiceInterface; use VuFind\ILS\Connection; use VuFind\OAuth2\Entity\UserEntity; @@ -225,7 +224,7 @@ public function testIdentityRepositoryWithFailingILS(): void */ protected function getMockUser(): UserEntityInterface { - $user = $this->createMock(User::class); + $user = $this->createMock(UserEntityInterface::class); $user->expects($this->any())->method('getId')->willReturn(2); $user->expects($this->any())->method('getFirstname')->willReturn('Lib'); $user->expects($this->any())->method('getLastname')->willReturn('Rarian'); diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php index 73c42a71872..55042b6ebb4 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php @@ -30,6 +30,8 @@ namespace VuFindTest\Role\PermissionProvider; use LmcRbacMvc\Service\AuthorizationService; +use PHPUnit\Framework\MockObject\MockObject; +use VuFind\Db\Entity\UserEntityInterface; /** * PermissionProvider User Test Class @@ -57,16 +59,16 @@ class UserTest extends \PHPUnit\Framework\TestCase protected $userValueMap = [ 'testuser1' => [ - ['username','mbeh'], - ['email','markus.beh@ub.uni-freiburg.de'], - ['college', 'Albert Ludwigs Universität Freiburg'], + ['username', 'mbeh'], + ['email', 'markus.beh@ub.uni-freiburg.de'], + ['college', 'Albert Ludwigs Universität Freiburg'], ], 'testuser2' => [ - ['username','mbeh2'], - ['email','markus.beh@ub.uni-freiburg.de'], - ['college', 'Villanova University'], - ['major', 'alumni'], + ['username', 'mbeh2'], + ['email', 'markus.beh@ub.uni-freiburg.de'], + ['college', 'Villanova University'], + ['major', 'alumni'], ], ]; @@ -137,7 +139,7 @@ protected function check($testuser, $options, $roles) protected function getMockAuthorizationService() { $authorizationService - = $this->getMockBuilder(\LmcRbacMvc\Service\AuthorizationService::class) + = $this->getMockBuilder(AuthorizationService::class) ->disableOriginalConstructor() ->getMock(); $authorizationService @@ -150,17 +152,17 @@ protected function getMockAuthorizationService() /** * Get a mock user object * - * @return \VuFind\Db\Row\User + * @return UserEntityInterface&MockObject */ - protected function getMockUser(): \VuFind\Db\Row\User + protected function getMockUser(): UserEntityInterface&MockObject { - $user = $this->getMockBuilder(\VuFind\Db\Row\User::class) - ->disableOriginalConstructor() - ->getMock(); - $user->method('__get') - ->will($this->returnValueMap($this->userValueMap[$this->testuser])); - $user->method('offsetGet') - ->will($this->returnValueMap($this->userValueMap[$this->testuser])); + $user = $this->createMock(UserEntityInterface::class); + + // Dynamically mock getter methods + foreach ($this->userValueMap[$this->testuser] ?? [] as $entry) { + [$property, $value] = $entry; + $user->method('get' . ucfirst($property))->willReturn($value); + } return $user; } diff --git a/module/VuFindApi/src/VuFindApi/Controller/SearchApiController.php b/module/VuFindApi/src/VuFindApi/Controller/SearchApiController.php index f8fb1a563a4..659ce028428 100644 --- a/module/VuFindApi/src/VuFindApi/Controller/SearchApiController.php +++ b/module/VuFindApi/src/VuFindApi/Controller/SearchApiController.php @@ -126,7 +126,7 @@ class SearchApiController extends \VuFind\Controller\AbstractSearch implements A /** * Facet configuration * - * @var \Laminas\Config\Config + * @var \VuFind\Config\Config */ protected $facetConfig; diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/AbstractExpireCommandTest.php b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/AbstractExpireCommandTest.php index e414955fcaf..a0910f111a1 100644 --- a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/AbstractExpireCommandTest.php +++ b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Command/Util/AbstractExpireCommandTest.php @@ -33,7 +33,6 @@ use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; use VuFind\Db\Service\Feature\DeleteExpiredInterface; -use VuFind\Db\Table\Gateway; use VuFindConsole\Command\Util\AbstractExpireCommand; /** @@ -171,13 +170,13 @@ public function testSuccessfulNonExpiration() /** * Get the command class * - * @param Gateway|DeleteExpiredInterface $service Table to process - * @param DateTime $date Expiration date threshold + * @param DeleteExpiredInterface $service Table to process + * @param DateTime $date Expiration date threshold * * @return MockObject&AbstractExpireCommand */ protected function getCommand( - Gateway|DeleteExpiredInterface $service, + DeleteExpiredInterface $service, DateTime $date ): MockObject&AbstractExpireCommand { $command = $this->getMockBuilder($this->targetClass) diff --git a/themes/bootstrap5/templates/comments/userlist.phtml b/themes/bootstrap5/templates/comments/userlist.phtml index be963d2c726..96eef1e79c8 100644 --- a/themes/bootstrap5/templates/comments/userlist.phtml +++ b/themes/bootstrap5/templates/comments/userlist.phtml @@ -76,7 +76,7 @@ - dateTime()->convertToDisplayDateAndTime('Y-m-d H:i:s', $comment['created'])?> + dateTime()->convertToDisplayDateAndTime('U', $comment['created']->getTimestamp())?> escapeHtml($comment['recordTitle'])?> diff --git a/themes/bootstrap5/templates/ratings/userlist.phtml b/themes/bootstrap5/templates/ratings/userlist.phtml index 67d9adc7d2a..64eb27c8cab 100644 --- a/themes/bootstrap5/templates/ratings/userlist.phtml +++ b/themes/bootstrap5/templates/ratings/userlist.phtml @@ -16,7 +16,7 @@

transEsc('Your Ratings')?>

- ratings) !== 0): ?> + ratings) !== 0): ?>