diff --git a/composer.json b/composer.json index 0a63e1fe847..4ddf9ac9670 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,8 @@ "colinmollenhour/credis": "1.16.1", "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", "guzzlehttp/guzzle": "7.9.2", "jaybizzle/crawler-detect": "^1.2", diff --git a/composer.lock b/composer.lock index ef9c297cad6..d3cd3f5d9cf 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": "b15ea90f88ec350a6fa2edb87cc785b9", + "content-hash": "37b2d8924ea86759db04c05c4615d4ef", "packages": [ { "name": "ahand/mobileesp", @@ -811,31 +811,865 @@ }, "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.2.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/d8af7f248c74f195f7347424600fd9e17b57af59", + "reference": "d8af7f248c74f195f7347424600fd9e17b57af59", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1", + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "ext-json": "*", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.11" + }, + "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.2.2" + }, + "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": "2024-04-18T06:56:21+00:00" + }, + { + "name": "doctrine/common", + "version": "3.4.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/6c8fef961f67b8bc802ce3e32e3ebd1022907286", + "reference": "6c8fef961f67b8bc802ce3e32e3ebd1022907286", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.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.4.5" + }, + "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": "2024-10-08T15:53:43+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.9.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "reference": "61446f07fcb522414d6cfd8b1c3e5f9e18c579ba", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "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" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.12.6", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.20", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.10.2", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "vimeo/psalm": "4.30.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.9.3" + }, + "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": "2024-10-10T17:56:43+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "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.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, + { + "name": "doctrine/doctrine-laminas-hydrator", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/doctrine-laminas-hydrator.git", + "reference": "a9969df745c9babbcb61ef42862e5ca1c6413a60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/doctrine-laminas-hydrator/zipball/a9969df745c9babbcb61ef42862e5ca1c6413a60", + "reference": "a9969df745c9babbcb61ef42862e5ca1c6413a60", + "shasum": "" + }, + "require": { + "doctrine/collections": "^2.0.0", + "doctrine/inflector": "^2.0.4", + "doctrine/persistence": "^3.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": "2024-11-28T13:46:59+00:00" + }, + { + "name": "doctrine/doctrine-module", + "version": "6.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineModule.git", + "reference": "ece08030bcabc0d2090aa69587c79417306fea79" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineModule/zipball/ece08030bcabc0d2090aa69587c79417306fea79", + "reference": "ece08030bcabc0d2090aa69587c79417306fea79", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "composer/semver": "^3.0", + "doctrine/annotations": "^1.13.3 || ^2", + "doctrine/cache": "^1.13.0 || ^2.1.0", + "doctrine/collections": "^1.8.0 || ^2.1", + "doctrine/doctrine-laminas-hydrator": "^3.2.0", + "doctrine/event-manager": "^1.2.0 || ^2.0", + "doctrine/inflector": "^2.0.6", + "doctrine/persistence": "^2.5.5 || ^3.1.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-modulemanager": "^2.12.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", + "laminas/laminas-validator": "^2.25.0", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/container": "^1.1.2", + "symfony/console": "^5.4.16 || ^6.2.1" + }, + "conflict": { + "doctrine/orm": "2.12.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "doctrine/mongodb-odm": "^2.5.0", + "doctrine/orm": "^2.13.4", + "jangregor/phpstan-prophecy": "^1.0.0", + "laminas/laminas-i18n": "^2.17.0", + "laminas/laminas-log": "^2.15.3", + "laminas/laminas-serializer": "^2.13.0", + "laminas/laminas-session": "^2.13.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-phpunit": "^1.3.0", + "phpunit/phpunit": "^9.5.27", + "predis/predis": "^1.1.10", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "doctrine/data-fixtures": "Data Fixtures if you want to generate test data or bootstrap data for your deployments" + }, + "bin": [ + "bin/doctrine-module" + ], + "type": "library", + "extra": { + "laminas": { + "config-provider": "DoctrineModule\\ConfigProvider", + "module": "DoctrineModule" + } + }, + "autoload": { + "psr-4": { + "DoctrineModule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Spraggs", + "email": "theman@spiffyjr.me", + "homepage": "http://www.spiffyjr.me/" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://marco-pivetta.com/" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@hotmail.com" + }, + { + "name": "Michaƫl Gallego", + "email": "mic.gallego@gmail.com", + "homepage": "http://www.michaelgallego.fr" + }, + { + "name": "Tom H Anderson", + "email": "tom.h.anderson@gmail.com", + "homepage": "https://tomhanderson.com" + } + ], + "description": "Laminas Module that provides Doctrine basic functionality required for ORM and ODM modules", + "homepage": "http://www.doctrine-project.org/", + "keywords": [ + "doctrine", + "laminas", + "module" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineModule/issues", + "source": "https://github.com/doctrine/DoctrineModule/tree/6.1.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%2Fdoctrine-module", + "type": "tidelift" + } + ], + "time": "2024-01-26T07:13:30+00:00" + }, + { + "name": "doctrine/doctrine-orm-module", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineORMModule.git", + "reference": "d9c7fa931d24e0c1e6c34dc9e43d6ddfa74877a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineORMModule/zipball/d9c7fa931d24e0c1e6c34dc9e43d6ddfa74877a5", + "reference": "d9c7fa931d24e0c1e6c34dc9e43d6ddfa74877a5", + "shasum": "" + }, + "require": { + "doctrine/dbal": "^2.13.7 || ^3.3.2", + "doctrine/doctrine-laminas-hydrator": "^3.0.0", + "doctrine/doctrine-module": "^5.3.0 || ^6.0.2", + "doctrine/event-manager": "^1.1.1", + "doctrine/orm": "^2.11.1", + "doctrine/persistence": "^2.3.0 || ^3.0.0", + "ext-json": "*", + "laminas/laminas-eventmanager": "^3.4.0", + "laminas/laminas-modulemanager": "^2.11.0", + "laminas/laminas-mvc": "^3.3.2", + "laminas/laminas-paginator": "^2.12.2", + "laminas/laminas-servicemanager": "^3.17.0", + "laminas/laminas-stdlib": "^3.7.1", + "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/container": "^1.1.2", + "symfony/console": "^5.4.3 || ^6.0.3" + }, + "conflict": { + "doctrine/migrations": "<3.3" + }, + "require-dev": { + "doctrine/annotations": "^1.13.2", + "doctrine/coding-standard": "^9.0.0", + "doctrine/data-fixtures": "^1.5.2", + "doctrine/migrations": "^3.4.1", + "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.13.0", + "laminas/laminas-log": "^2.15.0", + "laminas/laminas-serializer": "^2.12.0", + "ocramius/proxy-manager": "^2.2.0", + "phpstan/phpstan": "^1.4.6", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpunit/phpunit": "^9.5.13", + "squizlabs/php_codesniffer": "^3.6.2", + "vimeo/psalm": "^5.4.0" + }, + "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": { + "config-provider": "DoctrineORMModule\\ConfigProvider", + "module": "DoctrineORMModule" + } + }, + "autoload": { + "psr-4": { + "DoctrineORMModule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Spraggs", + "email": "theman@spiffyjr.me", + "homepage": "http://www.spiffyjr.me/" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://marco-pivetta.com/" + }, + { + "name": "Evan Coury", + "email": "me@evancoury.com", + "homepage": "http://blog.evan.pro/" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@hotmail.com" + }, + { + "name": "Tom H Anderson", + "email": "tom.h.anderson@gmail.com" + } + ], + "description": "Laminas Module that provides Doctrine ORM functionality", + "homepage": "http://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.1.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": "2024-01-25T22:03:02+00:00" + }, { "name": "doctrine/event-manager", - "version": "2.0.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/event-manager.git", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", - "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/95aa4cb529f1e96576f3fda9f5705ada4056a520", + "reference": "95aa4cb529f1e96576f3fda9f5705ada4056a520", "shasum": "" }, "require": { - "php": "^8.1" + "doctrine/deprecations": "^0.5.3 || ^1", + "php": "^7.1 || ^8.0" }, "conflict": { "doctrine/common": "<2.9" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.8.8", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.24" + "doctrine/coding-standard": "^9 || ^10", + "phpstan/phpstan": "~1.4.10 || ^1.8.8", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.24" }, "type": "library", "autoload": { @@ -870,21 +1704,182 @@ }, { "name": "Marco Pivetta", - "email": "ocramius@gmail.com" + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/1.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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:51:15+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": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" + "constructor", + "instantiate" ], "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -896,11 +1891,11 @@ "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", "type": "tidelift" } ], - "time": "2024-05-22T20:47:39+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "doctrine/lexer", @@ -979,6 +1974,109 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "doctrine/orm", + "version": "2.20.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/orm.git", + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/orm/zipball/8ed6c2234aba019f9737a6bcc9516438e62da27c", + "reference": "8ed6c2234aba019f9737a6bcc9516438e62da27c", + "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 || ^12.0", + "phpbench/phpbench": "^0.16.10 || ^1.0", + "phpstan/extension-installer": "~1.1.0 || ^1.4", + "phpstan/phpstan": "~1.4.10 || 1.12.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psr/log": "^1 || ^2 || ^3", + "squizlabs/php_codesniffer": "3.7.2", + "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", + "vimeo/psalm": "4.30.0 || 5.24.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/", + "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": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "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.0" + }, + "time": "2024-10-11T11:47:24+00:00" + }, { "name": "doctrine/persistence", "version": "3.4.0", @@ -1612,20 +2710,20 @@ }, { "name": "jaybizzle/crawler-detect", - "version": "v1.2.121", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/JayBizzle/Crawler-Detect.git", - "reference": "40ecda6322d4163fe2c6e1dd47c574f580b8487f" + "reference": "be155e11613fa618aa18aee438955588d1092a47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/40ecda6322d4163fe2c6e1dd47c574f580b8487f", - "reference": "40ecda6322d4163fe2c6e1dd47c574f580b8487f", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/be155e11613fa618aa18aee438955588d1092a47", + "reference": "be155e11613fa618aa18aee438955588d1092a47", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1.0" }, "require-dev": { "phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4" @@ -1658,9 +2756,84 @@ ], "support": { "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", - "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.121" + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.3.0" + }, + "time": "2024-11-25T19:38:36+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": "2024-10-20T21:42:39+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", @@ -1796,8 +2969,8 @@ "type": "library", "extra": { "laminas": { - "config-provider": "Laminas\\Cache\\Storage\\Adapter\\BlackHole\\ConfigProvider", - "module": "Laminas\\Cache\\Storage\\Adapter\\BlackHole" + "module": "Laminas\\Cache\\Storage\\Adapter\\BlackHole", + "config-provider": "Laminas\\Cache\\Storage\\Adapter\\BlackHole\\ConfigProvider" } }, "autoload": { @@ -1864,8 +3037,8 @@ "type": "library", "extra": { "laminas": { - "config-provider": "Laminas\\Cache\\Storage\\Adapter\\Filesystem\\ConfigProvider", - "module": "Laminas\\Cache\\Storage\\Adapter\\Filesystem" + "module": "Laminas\\Cache\\Storage\\Adapter\\Filesystem", + "config-provider": "Laminas\\Cache\\Storage\\Adapter\\Filesystem\\ConfigProvider" } }, "autoload": { @@ -2001,8 +3174,8 @@ "type": "library", "extra": { "laminas": { - "config-provider": "Laminas\\Cache\\Storage\\Adapter\\Memory\\ConfigProvider", - "module": "Laminas\\Cache\\Storage\\Adapter\\Memory" + "module": "Laminas\\Cache\\Storage\\Adapter\\Memory", + "config-provider": "Laminas\\Cache\\Storage\\Adapter\\Memory\\ConfigProvider" } }, "autoload": { @@ -2230,6 +3403,7 @@ "type": "community_bridge" } ], + "abandoned": true, "time": "2023-09-19T12:02:54+00:00" }, { @@ -2453,6 +3627,7 @@ "type": "community_bridge" } ], + "abandoned": "symfony/dom-crawler", "time": "2023-11-20T20:45:23+00:00" }, { @@ -3202,6 +4377,7 @@ "type": "community_bridge" } ], + "abandoned": true, "time": "2024-10-25T09:02:25+00:00" }, { @@ -3258,6 +4434,7 @@ "type": "community_bridge" } ], + "abandoned": true, "time": "2024-10-16T09:06:57+00:00" }, { @@ -6815,16 +7992,16 @@ }, { "name": "pear/pear-core-minimal", - "version": "v1.10.15", + "version": "v1.10.16", "source": { "type": "git", "url": "https://github.com/pear/pear-core-minimal.git", - "reference": "ce0adade8b97561656ace07cdaac4751c271ea8c" + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/ce0adade8b97561656ace07cdaac4751c271ea8c", - "reference": "ce0adade8b97561656ace07cdaac4751c271ea8c", + "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/c0f51b45f50683bf5bbf558036854ebc9b54d033", + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033", "shasum": "" }, "require": { @@ -6860,7 +8037,7 @@ "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR", "source": "https://github.com/pear/pear-core-minimal" }, - "time": "2024-03-16T18:41:45+00:00" + "time": "2024-11-24T22:27:58+00:00" }, { "name": "pear/pear_exception", @@ -7055,90 +8232,90 @@ "type": "library", "extra": { "phing-custom-taskdefs": { - "visualizer": "Phing\\Task\\Ext\\Visualizer\\VisualizerTask", - "gitarchive": "Phing\\Task\\Ext\\Git\\Git\\GitArchiveTask", - "gitinit": "Phing\\Task\\Ext\\Git\\Git\\GitInitTask", - "gitclone": "Phing\\Task\\Ext\\Git\\Git\\GitCloneTask", - "gitgc": "Phing\\Task\\Ext\\Git\\Git\\GitGcTask", - "gitbranch": "Phing\\Task\\Ext\\Git\\Git\\GitBranchTask", - "gitfetch": "Phing\\Task\\Ext\\Git\\Git\\GitFetchTask", - "gitmerge": "Phing\\Task\\Ext\\Git\\Git\\GitMergeTask", - "gitcheckout": "Phing\\Task\\Ext\\Git\\Git\\GitCheckoutTask", - "gitpull": "Phing\\Task\\Ext\\Git\\Git\\GitPullTask", - "gitpush": "Phing\\Task\\Ext\\Git\\Git\\GitPushTask", - "gitlog": "Phing\\Task\\Ext\\Git\\Git\\GitLogTask", - "gittag": "Phing\\Task\\Ext\\Git\\Git\\GitTagTask", - "gitcommit": "Phing\\Task\\Ext\\Git\\Git\\GitCommitTask", - "gitdescribe": "Phing\\Task\\Ext\\Git\\Git\\GitDescribeTask", - "coverage-setup": "Phing\\Task\\Ext\\Coverage\\CoverageSetupTask", - "coverage-merger": "Phing\\Task\\Ext\\Coverage\\CoverageMergerTask", - "coverage-report": "Phing\\Task\\Ext\\Coverage\\CoverageReportTask", - "coverage-threshold": "Phing\\Task\\Ext\\Coverage\\CoverageThresholdTask", - "phpstan": "Phing\\Task\\Ext\\Analyzer\\Phpstan\\PHPStanTask", - "phpmd": "Phing\\Task\\Ext\\Analyzer\\Phpmd\\PHPMDTask", - "phpdepend": "Phing\\Task\\Ext\\Analyzer\\Pdepend\\PhpDependTask", - "sonar": "Phing\\Task\\Ext\\Analyzer\\Sonar\\SonarTask", - "phkpackage": "Phing\\Task\\Ext\\PhkPackage\\PhkPackageTask", + "scp": "Phing\\Task\\Ext\\Ssh\\ScpTask", + "ssh": "Phing\\Task\\Ext\\Ssh\\SshTask", + "tar": "Phing\\Task\\Ext\\Archive\\TarTask", + "zip": "Phing\\Task\\Ext\\Archive\\ZipTask", "sass": "Phing\\Task\\Ext\\Sass\\SassTask", + "gitgc": "Phing\\Task\\Ext\\Git\\Git\\GitGcTask", + "hgadd": "Phing\\Task\\Ext\\Hg\\HgAddTask", + "hglog": "Phing\\Task\\Ext\\Hg\\HgLogTask", + "hgtag": "Phing\\Task\\Ext\\Hg\\HgTagTask", "jsmin": "Phing\\Task\\Ext\\JsMin\\JsMinTask", - "liquibase-changelog": "Phing\\Task\\Ext\\Liquibase\\LiquibaseChangeLogTask", - "liquibase-dbdoc": "Phing\\Task\\Ext\\Liquibase\\LiquibaseDbDocTask", - "liquibase-diff": "Phing\\Task\\Ext\\Liquibase\\LiquibaseDiffTask", - "liquibase-rollback": "Phing\\Task\\Ext\\Liquibase\\LiquibaseRollbackTask", - "liquibase-tag": "Phing\\Task\\Ext\\Liquibase\\LiquibaseTagTask", - "liquibase-update": "Phing\\Task\\Ext\\Liquibase\\LiquibaseUpdateTask", - "liquibase": "Phing\\Task\\Ext\\Liquibase\\LiquibaseTask", + "phpmd": "Phing\\Task\\Ext\\Analyzer\\Phpmd\\PHPMDTask", "s3get": "Phing\\Task\\Ext\\Amazon\\S3\\S3GetTask", "s3put": "Phing\\Task\\Ext\\Amazon\\S3\\S3PutTask", - "zsdtvalidate": "Phing\\Task\\Ext\\ZendServerDeploymentTool\\ZsdtValidateTask", - "zsdtpack": "Phing\\Task\\Ext\\ZendServerDeploymentTool\\ZsdtPackTask", - "hgadd": "Phing\\Task\\Ext\\Hg\\HgAddTask", - "hgarchive": "Phing\\Task\\Ext\\Hg\\HgArchiveTask", - "hgclone": "Phing\\Task\\Ext\\Hg\\HgCloneTask", - "hgcommit": "Phing\\Task\\Ext\\Hg\\HgCommitTask", + "sonar": "Phing\\Task\\Ext\\Analyzer\\Sonar\\SonarTask", + "untar": "Phing\\Task\\Ext\\Archive\\UntarTask", + "unzip": "Phing\\Task\\Ext\\Archive\\UnzipTask", + "apigen": "Phing\\Task\\Ext\\ApiGen\\ApiGenTask", + "gitlog": "Phing\\Task\\Ext\\Git\\Git\\GitLogTask", + "gittag": "Phing\\Task\\Ext\\Git\\Git\\GitTagTask", "hginit": "Phing\\Task\\Ext\\Hg\\HgInitTask", - "hglog": "Phing\\Task\\Ext\\Hg\\HgLogTask", "hgpull": "Phing\\Task\\Ext\\Hg\\HgPullTask", "hgpush": "Phing\\Task\\Ext\\Hg\\HgPushTask", - "hgrevert": "Phing\\Task\\Ext\\Hg\\HgRevertTask", - "hgtag": "Phing\\Task\\Ext\\Hg\\HgTagTask", - "hgupdate": "Phing\\Task\\Ext\\Hg\\HgUpdateTask", - "http-request": "Phing\\Task\\Ext\\Http\\HttpRequestTask", - "httpget": "Phing\\Task\\Ext\\Http\\HttpGetTask", - "phpunit": "Phing\\Task\\Ext\\PhpUnit\\PHPUnitTask", - "phpunitreport": "Phing\\Task\\Ext\\PhpUnit\\PHPUnitReportTask", - "apigen": "Phing\\Task\\Ext\\ApiGen\\ApiGenTask", - "ssh": "Phing\\Task\\Ext\\Ssh\\SshTask", - "scp": "Phing\\Task\\Ext\\Ssh\\ScpTask", - "dbdeploy": "Phing\\Task\\Ext\\DbDeploy\\DbDeployTask", - "smarty": "Phing\\Task\\Ext\\Snmarty\\SmartyTask", - "ioncubeencoder": "Phing\\Task\\Ext\\Ioncube\\IoncubeEncoderTask", - "ioncubelicense": "Phing\\Task\\Ext\\Ioncube\\IoncubeLicenseTask", - "tar": "Phing\\Task\\Ext\\Archive\\TarTask", - "untar": "Phing\\Task\\Ext\\Archive\\UntarTask", - "zip": "Phing\\Task\\Ext\\Archive\\ZipTask", - "unzip": "Phing\\Task\\Ext\\Archive\\UnzipTask", "jshint": "Phing\\Task\\Ext\\JsHint\\JsHintTask", - "zendcodeanalyzer": "Phing\\Task\\Ext\\ZendCodeAnalyzer\\ZendCodeAnalyzerTask", + "phpdoc": "Phing\\Task\\Ext\\PhpDoc\\PhpDocumentor2Task", + "smarty": "Phing\\Task\\Ext\\Snmarty\\SmartyTask", + "svnlog": "Phing\\Task\\Ext\\Svn\\SvnLogTask", "analyze": "Phing\\Task\\Ext\\ZendCodeAnalyzer\\ZendCodeAnalyzerTask", + "gitinit": "Phing\\Task\\Ext\\Git\\Git\\GitInitTask", + "gitpull": "Phing\\Task\\Ext\\Git\\Git\\GitPullTask", + "gitpush": "Phing\\Task\\Ext\\Git\\Git\\GitPushTask", + "hgclone": "Phing\\Task\\Ext\\Hg\\HgCloneTask", + "httpget": "Phing\\Task\\Ext\\Http\\HttpGetTask", "inifile": "Phing\\Task\\Ext\\IniFile\\IniFileTask", - "phpdoc": "Phing\\Task\\Ext\\PhpDoc\\PhpDocumentor2Task", "phpdoc2": "Phing\\Task\\Ext\\PhpDoc\\PhpDocumentor2Task", + "phpstan": "Phing\\Task\\Ext\\Analyzer\\Phpstan\\PHPStanTask", + "phpunit": "Phing\\Task\\Ext\\PhpUnit\\PHPUnitTask", + "svncopy": "Phing\\Task\\Ext\\Svn\\SvnCopyTask", + "svninfo": "Phing\\Task\\Ext\\Svn\\SvnInfoTask", + "svnlist": "Phing\\Task\\Ext\\Svn\\SvnListTask", + "dbdeploy": "Phing\\Task\\Ext\\DbDeploy\\DbDeployTask", + "gitclone": "Phing\\Task\\Ext\\Git\\Git\\GitCloneTask", + "gitfetch": "Phing\\Task\\Ext\\Git\\Git\\GitFetchTask", + "gitmerge": "Phing\\Task\\Ext\\Git\\Git\\GitMergeTask", + "hgcommit": "Phing\\Task\\Ext\\Hg\\HgCommitTask", + "hgrevert": "Phing\\Task\\Ext\\Hg\\HgRevertTask", + "hgupdate": "Phing\\Task\\Ext\\Hg\\HgUpdateTask", + "zsdtpack": "Phing\\Task\\Ext\\ZendServerDeploymentTool\\ZsdtPackTask", "ftpdeploy": "Phing\\Task\\Ext\\FtpDeploy\\FtpDeployTask", - "svnlastrevision": "Phing\\Task\\Ext\\Svn\\SvnLastRevisionTask", - "svncheckout": "Phing\\Task\\Ext\\Svn\\SvnCheckoutTask", + "gitbranch": "Phing\\Task\\Ext\\Git\\Git\\GitBranchTask", + "gitcommit": "Phing\\Task\\Ext\\Git\\Git\\GitCommitTask", + "hgarchive": "Phing\\Task\\Ext\\Hg\\HgArchiveTask", + "liquibase": "Phing\\Task\\Ext\\Liquibase\\LiquibaseTask", + "phpdepend": "Phing\\Task\\Ext\\Analyzer\\Pdepend\\PhpDependTask", + "svncommit": "Phing\\Task\\Ext\\Svn\\SvnCommitTask", "svnexport": "Phing\\Task\\Ext\\Svn\\SvnExportTask", - "svnupdate": "Phing\\Task\\Ext\\Svn\\SvnUpdateTask", + "svnrevert": "Phing\\Task\\Ext\\Svn\\SvnRevertTask", "svnswitch": "Phing\\Task\\Ext\\Svn\\SvnSwitchTask", - "svncopy": "Phing\\Task\\Ext\\Svn\\SvnCopyTask", - "svncommit": "Phing\\Task\\Ext\\Svn\\SvnCommitTask", - "svnlist": "Phing\\Task\\Ext\\Svn\\SvnListTask", - "svnlog": "Phing\\Task\\Ext\\Svn\\SvnLogTask", - "svninfo": "Phing\\Task\\Ext\\Svn\\SvnInfoTask", - "svnproplist": "Phing\\Task\\Ext\\Svn\\SvnProplistTask", + "svnupdate": "Phing\\Task\\Ext\\Svn\\SvnUpdateTask", + "gitarchive": "Phing\\Task\\Ext\\Git\\Git\\GitArchiveTask", + "phkpackage": "Phing\\Task\\Ext\\PhkPackage\\PhkPackageTask", "svnpropget": "Phing\\Task\\Ext\\Svn\\SvnPropgetTask", "svnpropset": "Phing\\Task\\Ext\\Svn\\SvnPropsetTask", - "svnrevert": "Phing\\Task\\Ext\\Svn\\SvnRevertTask" + "visualizer": "Phing\\Task\\Ext\\Visualizer\\VisualizerTask", + "gitcheckout": "Phing\\Task\\Ext\\Git\\Git\\GitCheckoutTask", + "gitdescribe": "Phing\\Task\\Ext\\Git\\Git\\GitDescribeTask", + "svncheckout": "Phing\\Task\\Ext\\Svn\\SvnCheckoutTask", + "svnproplist": "Phing\\Task\\Ext\\Svn\\SvnProplistTask", + "http-request": "Phing\\Task\\Ext\\Http\\HttpRequestTask", + "zsdtvalidate": "Phing\\Task\\Ext\\ZendServerDeploymentTool\\ZsdtValidateTask", + "liquibase-tag": "Phing\\Task\\Ext\\Liquibase\\LiquibaseTagTask", + "phpunitreport": "Phing\\Task\\Ext\\PhpUnit\\PHPUnitReportTask", + "coverage-setup": "Phing\\Task\\Ext\\Coverage\\CoverageSetupTask", + "ioncubeencoder": "Phing\\Task\\Ext\\Ioncube\\IoncubeEncoderTask", + "ioncubelicense": "Phing\\Task\\Ext\\Ioncube\\IoncubeLicenseTask", + "liquibase-diff": "Phing\\Task\\Ext\\Liquibase\\LiquibaseDiffTask", + "coverage-merger": "Phing\\Task\\Ext\\Coverage\\CoverageMergerTask", + "coverage-report": "Phing\\Task\\Ext\\Coverage\\CoverageReportTask", + "liquibase-dbdoc": "Phing\\Task\\Ext\\Liquibase\\LiquibaseDbDocTask", + "svnlastrevision": "Phing\\Task\\Ext\\Svn\\SvnLastRevisionTask", + "liquibase-update": "Phing\\Task\\Ext\\Liquibase\\LiquibaseUpdateTask", + "zendcodeanalyzer": "Phing\\Task\\Ext\\ZendCodeAnalyzer\\ZendCodeAnalyzerTask", + "coverage-threshold": "Phing\\Task\\Ext\\Coverage\\CoverageThresholdTask", + "liquibase-rollback": "Phing\\Task\\Ext\\Liquibase\\LiquibaseRollbackTask", + "liquibase-changelog": "Phing\\Task\\Ext\\Liquibase\\LiquibaseChangeLogTask" }, "phing-custom-typedefs": { "sshconfig": "Phing\\Task\\Ext\\Ssh\\Ssh2MethodParam", @@ -8371,16 +9548,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { @@ -8418,7 +9595,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" }, "funding": [ { @@ -8434,7 +9611,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/event-dispatcher", @@ -8518,16 +9695,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { @@ -8574,7 +9751,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -8590,7 +9767,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/filesystem", @@ -8825,16 +10002,16 @@ }, { "name": "symfony/options-resolver", - "version": "v6.4.13", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db" + "reference": "368128ad168f20e22c32159b9f761e456cec0c78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0a62a9f2504a8dd27083f89d21894ceb01cc59db", - "reference": "0a62a9f2504a8dd27083f89d21894ceb01cc59db", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/368128ad168f20e22c32159b9f761e456cec0c78", + "reference": "368128ad168f20e22c32159b9f761e456cec0c78", "shasum": "" }, "require": { @@ -8872,7 +10049,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.13" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.16" }, "funding": [ { @@ -8888,7 +10065,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:18:03+00:00" + "time": "2024-11-20T10:57:02+00:00" }, { "name": "symfony/polyfill-ctype", @@ -9509,16 +10686,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -9572,7 +10749,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -9588,7 +10765,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", @@ -13988,16 +15165,16 @@ }, { "name": "symfony/dependency-injection", - "version": "v6.4.15", + "version": "v6.4.16", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "70ab1f65a4516ef741e519ea938e6aa465e6aa36" + "reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/70ab1f65a4516ef741e519ea938e6aa465e6aa36", - "reference": "70ab1f65a4516ef741e519ea938e6aa465e6aa36", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7a379d8871f6a36f01559c14e11141cc02eb8dc8", + "reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8", "shasum": "" }, "require": { @@ -14049,7 +15226,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.15" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.16" }, "funding": [ { @@ -14065,7 +15242,7 @@ "type": "tidelift" } ], - "time": "2024-11-09T06:56:25+00:00" + "time": "2024-11-25T14:52:46+00:00" }, { "name": "symfony/finder", 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..bf3007790b2 --- /dev/null +++ b/config/cli-config.php @@ -0,0 +1,6 @@ +getServiceManager()->get('doctrine.entity_manager.orm_default') +); diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index cf00057fce8..e20e0718f18 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -447,6 +447,8 @@ 'VuFind\Crypt\SecretCalculator' => 'VuFind\Crypt\SecretCalculatorFactory', 'VuFind\Date\Converter' => 'VuFind\Service\DateConverterFactory', 'VuFind\Db\AdapterFactory' => 'VuFind\Service\ServiceWithConfigIniFactory', + 'VuFind\Db\Connection' => 'VuFind\Db\ConnectionFactory', + 'VuFind\Db\Entity\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', 'VuFind\Db\Row\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', 'VuFind\Db\Service\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', 'VuFind\Db\Table\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory', @@ -549,6 +551,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', @@ -622,6 +626,44 @@ '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_annotation_driver' => [ + 'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class, + 'cache' => 'filesystem', + 'paths' => [ + 'module/VuFind/src/VuFind/Db/Entity', + ], + ], + 'orm_default' => [ + 'drivers' => [ + 'VuFind\Db\Entity' => 'vufind_annotation_driver', + ], + ], + ], + 'entitymanager' => [ + 'orm_vufind' => [ + 'connection' => 'orm_vufind', + 'configuration' => 'orm_vufind', + ], + ], + ], 'translator' => [], 'translator_plugins' => [ 'factories' => [ @@ -686,6 +728,7 @@ '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_entity' => [ /* see VuFind\Db\Entity\PluginManager for defaults */ ], 'db_row' => [ /* see VuFind\Db\Row\PluginManager for defaults */ ], 'db_service' => [ /* see VuFind\Db\Service\PluginManager for defaults */ ], 'db_table' => [ /* see VuFind\Db\Table\PluginManager for defaults */ ], diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php index 55d44e82cc5..ce7e0d7bde0 100644 --- a/module/VuFind/src/VuFind/Auth/Manager.php +++ b/module/VuFind/src/VuFind/Auth/Manager.php @@ -53,8 +53,10 @@ */ class Manager implements \LmcRbacMvc\Identity\IdentityProviderInterface, - \Laminas\Log\LoggerAwareInterface + \Laminas\Log\LoggerAwareInterface, + \VuFind\Db\Table\DbTableAwareInterface { + use \VuFind\Db\Table\DbTableAwareTrait; use \VuFind\Log\LoggerAwareTrait; /** @@ -549,7 +551,15 @@ public function getUserObject(): ?UserEntityInterface if (null === $this->currentUser) { $this->logout(''); } + // Temporary backward-compatibility shim while we transition from Laminas to Doctrine: + if ($this->currentUser && !($this->currentUser instanceof \VuFind\Db\Row\User)) { + $this->currentUser = $this->getDbTable('User')->getById($this->currentUser->getId()); + } } elseif ($user = $this->loginTokenManager->tokenLogin($this->sessionManager->getId())) { + // Temporary backward-compatibility shim while we transition from Laminas to Doctrine: + if ($user && !($user instanceof \VuFind\Db\Row\User)) { + $user = $this->getDbTable('User')->getById($user->getId()); + } if ($this->getAuth() instanceof ChoiceAuth) { $this->getAuth()->setStrategy($user->getAuthMethod()); } diff --git a/module/VuFind/src/VuFind/Auth/ManagerFactory.php b/module/VuFind/src/VuFind/Auth/ManagerFactory.php index 18c3440f3bf..4b590a14f47 100644 --- a/module/VuFind/src/VuFind/Auth/ManagerFactory.php +++ b/module/VuFind/src/VuFind/Auth/ManagerFactory.php @@ -91,6 +91,7 @@ public function __invoke( $loginTokenManager, $ils ); + $manager->setDbTableManager($container->get(\VuFind\Db\Table\PluginManager::class)); $manager->setIlsAuthenticator($container->get(\VuFind\Auth\ILSAuthenticator::class)); $manager->checkForExpiredCredentials(); return $manager; diff --git a/module/VuFind/src/VuFind/Db/Table/Shortlinks.php b/module/VuFind/src/VuFind/Db/Connection.php similarity index 50% rename from module/VuFind/src/VuFind/Db/Table/Shortlinks.php rename to module/VuFind/src/VuFind/Db/Connection.php index f40da818903..2bfd126abd5 100644 --- a/module/VuFind/src/VuFind/Db/Table/Shortlinks.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\Table; - -use Laminas\Db\Adapter\Adapter; -use VuFind\Db\Row\RowGateway; +namespace VuFind\Db; /** - * Table Definition for shortlinks + * Wrapper class for VuFind Doctrine connections. * * @category VuFind - * @package Db_Table - * @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 Shortlinks extends Gateway +class Connection extends \Doctrine\DBAL\Connection { - /** - * 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/ConnectionFactory.php b/module/VuFind/src/VuFind/Db/ConnectionFactory.php new file mode 100644 index 00000000000..aa882a20059 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/ConnectionFactory.php @@ -0,0 +1,311 @@ + + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Site + */ + +namespace VuFind\Db; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DriverManager; +use Laminas\Config\Config; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Exception\ServiceNotFoundException; +use Psr\Container\ContainerExceptionInterface as ContainerException; +use Psr\Container\ContainerInterface; +use VuFind\Config\Feature\SecretTrait; + +/** + * Factory for Doctrine connection. May be used as a service or as a standard + * Laminas factory. + * + * @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 ConnectionFactory implements \Laminas\ServiceManager\Factory\FactoryInterface +{ + use SecretTrait; + + /** + * VuFind configuration + * + * @var Config + */ + protected $config; + + /** + * Configuration file name when used as a factory. + * + * @var string + */ + protected string $configName = 'config'; + + /** + * Connection wrapper class. + * + * @var string + */ + protected string $wrapperClass = \VuFind\Db\Connection::class; + + /** + * Constructor + * + * @param Config $config VuFind configuration (provided when used + * as service; omitted when used as factory) + * @param ?ContainerInterface $container Service container (provided when used + * as service; omitted when used as factory) + */ + public function __construct( + Config $config = null, + protected ?ContainerInterface $container = null + ) { + $this->config = $config ?: new Config([]); + } + + /** + * Create an object (glue code for FactoryInterface compliance) + * + * @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!'); + } + $this->config = $container->get(\VuFind\Config\PluginManager::class) + ->get($this->configName); + $this->container = $container; + return $this->getConnection(); + } + + /** + * Obtain a Laminas\DB 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 Connection + */ + 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)) { + $options = $this->getOptionsFromConnectionString( + $this->config->Database->database, + $overrideUser, + $overridePass + ); + } else { + $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; + } + } + + /* TODO: still needed? + $options['use_ssl'] = $this->config->Database->use_ssl ?? false; + $options['driver_options'] = $this->getDriverOptions($driverName); + */ + + // 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->getConnectionFromOptions($options + $extraOptions); + } + + /** + * Translate the connection string protocol into a driver name. + * + * @param string $type Database type from connection string + * + * @return string + */ + public function getDriverName($type) + { + switch (strtolower($type)) { + case 'mysql': + return 'pdo_mysql'; + case 'oci8': + // TODO: fix/test + return 'Oracle'; + case 'pgsql': + return 'pdo_pgsql'; + } + return $type; + } + + /** + * Get options for the selected driver. + * + * @param string $driver Driver name + * + * @return array + */ + protected function getDriverOptions($driver) + { + switch ($driver) { + case 'mysqli': + return ($this->config->Database->verify_server_certificate ?? false) + ? [] : [MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]; + } + return []; + } + + /** + * Obtain a Laminas\DB connection using an option array. + * + * @param array $options Options for building adapter + * + * @return Connection + */ + public function getConnectionFromOptions($options) + { + /* TODO: still needed? + // Set up custom options by database type: + $driver = strtolower($options['driver']); + switch ($driver) { + case 'mysqli': + $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.' + ); + } + $options['options'] = ['buffer_results' => true]; + break; + } + */ + $options['wrapperClass'] = $this->wrapperClass; + + // Set up database connection: + if (empty($this->container)) { + throw new \Exception('Container is missing!'); + } + $connection = DriverManager::getConnection( + $options + ); + + /* TODO: still needed? + // 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(); + } + */ + + return $connection; + } + + /** + * Parse Laminas\DB connection options from a connection string. + * + * @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 + * from connection string) + * @param ?string $overridePass Password override (leave null to use password + * from connection string) + * + * @return array + */ + public function getOptionsFromConnectionString( + string $connectionString, + ?string $overrideUser = null, + ?string $overridePass = null + ): array { + [$type, $details] = explode('://', $connectionString); + preg_match('/(.+)@([^@]+)\/(.+)/', $details, $matches); + $credentials = $matches[1] ?? null; + $host = $port = null; + if (isset($matches[2])) { + if (str_contains($matches[2], ':')) { + [$host, $port] = explode(':', $matches[2]); + } else { + $host = $matches[2]; + } + } + $dbName = $matches[3] ?? null; + if (strstr($credentials, ':')) { + [$username, $password] = explode(':', $credentials, 2); + } else { + $username = $credentials; + $password = null; + } + $username = $overrideUser ?? $username; + $password = $overridePass ?? $password; + + $driverName = $this->getDriverName($type); + + // Set up default options: + $options = [ + 'driver' => $driverName, + 'host' => $host, + 'user' => $username, + 'password' => $password, + 'dbname' => $dbName, + ]; + if (!empty($port)) { + $options['port'] = $port; + } + return $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..64c09e66d4f --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/AccessToken.php @@ -0,0 +1,260 @@ + + * @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 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\Entity + */ +class AccessToken implements AccessTokenEntityInterface +{ + /** + * Unique ID. + * + * @var string + * + * @ORM\Column(name="id", + * type="string", + * length=255, + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="NONE") + */ + protected $id; + + /** + * Token type. + * + * @var string + * + * @ORM\Column(name="type", + * type="string", + * length=128, + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="NONE") + */ + protected $type; + + /** + * User. + * + * @var UserEntityInterface + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * Data. + * + * @var ?string + * + * @ORM\Column(name="data", type="text", length=16777215, nullable=true) + */ + protected $data; + + /** + * Flag indicating status of the token. + * + * @var bool + * + * @ORM\Column(name="revoked", type="boolean", nullable=false) + */ + protected $revoked = '0'; + + /** + * 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 (bool)$this->revoked; + } + + /** + * Set revoked status. + * + * @param bool $revoked Revoked + * + * @return static + */ + public function setRevoked(bool $revoked): static + { + $this->revoked = $revoked ? '1' : '0'; + return $this; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php b/module/VuFind/src/VuFind/Db/Entity/AccessTokenEntityInterface.php index baa8bd89cfb..1a6f39b1ed9 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 ID. + * + * @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 57% rename from module/VuFind/src/VuFind/Db/Row/AuthHash.php rename to module/VuFind/src/VuFind/Db/Entity/AuthHash.php index de5a60da0ac..3a29182f372 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property string $session_id - * @property string $hash - * @property string $type - * @property string $data - * @property string $created + * @ORM\Table(name="auth_hash", + * uniqueConstraints={@ORM\UniqueConstraint(name="hash_type", + * columns={"hash", "type"})}, + * indexes={@ORM\Index(name="created", columns={"created"}), + * @ORM\Index(name="session_id", columns={"session_id"})} + * ) + * @ORM\Entity */ -class AuthHash extends RowGateway implements AuthHashEntityInterface, DbServiceAwareInterface +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 $id; /** - * Constructor + * Session ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var ?string + * + * @ORM\Column(name="session_id", type="string", length=128, nullable=true) */ - public function __construct($adapter) - { - parent::__construct('id', 'auth_hash', $adapter); - } + protected $sessionId; + + /** + * Hash value. + * + * @var string + * + * @ORM\Column(name="hash", type="string", length=255, nullable=false) + */ + protected $hash = ''; + + /** + * Type of the hash. + * + * @var ?string + * + * @ORM\Column(name="type", type="string", length=50, nullable=true) + */ + protected $type; + + /** + * Data. + * + * @var ?string + * + * @ORM\Column(name="data", type="text", length=16777215, nullable=true) + */ + protected $data; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $created = 'CURRENT_TIMESTAMP'; /** * Get identifier (returns null for an uninitialized or non-persisted object). @@ -75,7 +122,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -85,7 +132,7 @@ public function getId(): ?int */ public function getSessionId(): ?string { - return $this->session_id ?? null; + return $this->sessionId; } /** @@ -97,7 +144,7 @@ public function getSessionId(): ?string */ public function setSessionId(?string $sessionId): static { - $this->session_id = $sessionId; + $this->sessionId = $sessionId; return $this; } @@ -108,7 +155,7 @@ public function setSessionId(?string $sessionId): static */ public function getHash(): string { - return $this->hash ?? ''; + return $this->hash; } /** @@ -131,7 +178,7 @@ public function setHash(string $hash): static */ public function getHashType(): ?string { - return $this->type ?? null; + return $this->type; } /** @@ -154,7 +201,7 @@ public function setHashType(?string $type): static */ public function getData(): ?string { - return $this->__get('data'); + return $this->data; } /** @@ -166,7 +213,7 @@ public function getData(): ?string */ public function setData(?string $data): static { - $this->__set('data', $data); + $this->data = $data; return $this; } @@ -177,7 +224,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 +236,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 61% rename from module/VuFind/src/VuFind/Db/Row/ChangeTracker.php rename to module/VuFind/src/VuFind/Db/Entity/ChangeTracker.php index e3f15904e1c..bb2f7860712 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property string $core - * @property string $id - * @property ?string $first_indexed - * @property ?string $last_indexed - * @property ?string $last_record_change - * @property ?string $deleted + * @ORM\Table(name="change_tracker", + * indexes={@ORM\Index(name="deleted_index", columns={"deleted"})} + * ) + * @ORM\Entity */ -class ChangeTracker extends RowGateway implements ChangeTrackerEntityInterface +class ChangeTracker implements ChangeTrackerEntityInterface { /** - * Constructor + * Solr core containing record. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var string + * + * @ORM\Column(name="core", + * type="string", + * length=30, + * nullable=false + * ) + * @ORM\Id */ - public function __construct($adapter) - { - parent::__construct(['core', 'id'], 'change_tracker', $adapter); - } + protected $core; + + /** + * Id of record within core. + * + * @var string + * + * @ORM\Column(name="id", + * type="string", + * length=120, + * nullable=false + * ) + * @ORM\Id + */ + protected $id; + + /** + * First time added to index + * + * @var ?DateTime + * + * @ORM\Column(name="first_indexed", type="datetime", nullable=true) + */ + protected $firstIndexed; + + /** + * Last time changed in index. + * + * @var ?DateTime + * + * @ORM\Column(name="last_indexed", type="datetime", nullable=true) + */ + protected $lastIndexed; + + /** + * Last time original record was edited. + * + * @var ?DateTime + * + * @ORM\Column(name="last_record_change", type="datetime", nullable=true) + */ + protected $lastRecordChange; + + /** + * Time record was removed from index. + * + * @var ?DateTime + * + * @ORM\Column(name="deleted", type="datetime", nullable=true) + */ + protected $deleted; /** * Setter for identifier. @@ -115,7 +167,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 +178,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 +190,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 +201,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 +213,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 +224,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 +236,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 +247,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/Row/Comments.php b/module/VuFind/src/VuFind/Db/Entity/Comments.php similarity index 58% rename from module/VuFind/src/VuFind/Db/Row/Comments.php rename to module/VuFind/src/VuFind/Db/Entity/Comments.php index dbe7d52ad50..c8c221ae450 100644 --- a/module/VuFind/src/VuFind/Db/Row/Comments.php +++ b/module/VuFind/src/VuFind/Db/Entity/Comments.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\CommentsEntityInterface; -use VuFind\Db\Entity\ResourceEntityInterface; -use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Service\DbServiceAwareInterface; -use VuFind\Db\Service\UserServiceInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for comments + * Comments * * @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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property ?int $user_id - * @property int $resource_id - * @property string $comment - * @property string $created + * @ORM\Table(name="comments") + * @ORM\Entity */ -class Comments extends RowGateway implements CommentsEntityInterface, DbServiceAwareInterface +class Comments implements CommentsEntityInterface { - use \VuFind\Db\Service\DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; /** - * Constructor + * Comment. + * + * @var string * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @ORM\Column(name="comment", type="text", length=65535, nullable=false) */ - public function __construct($adapter) - { - parent::__construct('id', 'comments', $adapter); - } + protected $comment; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * User ID. + * + * @var ?User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Resource ID. + * + * @var Resource + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\Resource") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="resource_id", + * referencedColumnName="id") + * }) + */ + protected $resource; /** * Id getter @@ -107,7 +150,7 @@ public function getComment(): string */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } @@ -118,7 +161,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; } /** @@ -130,7 +173,7 @@ public function getCreated(): DateTime */ public function setUser(?UserEntityInterface $user): static { - $this->user_id = $user ? $user->getId() : null; + $this->user = $user; return $this; } @@ -141,9 +184,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; } /** @@ -155,7 +196,7 @@ public function getUser(): ?UserEntityInterface */ public function setResource(ResourceEntityInterface $resource): static { - $this->resource_id = $resource->getId(); + $this->resource = $resource; return $this; } } diff --git a/module/VuFind/src/VuFind/Db/Entity/ExternalSession.php b/module/VuFind/src/VuFind/Db/Entity/ExternalSession.php new file mode 100644 index 00000000000..d0a1b0beafe --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/ExternalSession.php @@ -0,0 +1,181 @@ + + * @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; + +/** + * ExternalSession + * + * @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="external_session", + * uniqueConstraints={@ORM\UniqueConstraint(name="session_id", + * columns={"session_id"})}, + * indexes={@ORM\Index(name="external_session_id", columns={"external_session_id"})} + * ) + * @ORM\Entity + */ +class ExternalSession implements ExternalSessionEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="bigint", + * nullable=false, + * options={"unsigned"=true} + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Session ID. + * + * @var string + * + * @ORM\Column(name="session_id", type="string", length=128, nullable=false) + */ + protected $sessionId; + + /** + * External session ID. + * + * @var string + * + * @ORM\Column(name="external_session_id", + * type="string", + * length=255, + * nullable=false + * ) + */ + protected $externalSessionId; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get PHP session id string. + * + * @return string + */ + public function getSessionId(): string + { + return $this->sessionId; + } + + /** + * Set PHP session id string. + * + * @param string $sessionId PHP session id string + * + * @return static + */ + public function setSessionId(string $sessionId): static + { + $this->sessionId = $sessionId; + return $this; + } + + /** + * Get external session id string. + * + * @return string + */ + public function getExternalSessionId(): string + { + return $this->externalSessionId; + } + + /** + * Set external session id string. + * + * @param string $externalSessionId External session id string + * + * @return static + */ + public function setExternalSessionId(string $externalSessionId): static + { + $this->externalSessionId = $externalSessionId; + 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; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/Feedback.php b/module/VuFind/src/VuFind/Db/Entity/Feedback.php similarity index 50% rename from module/VuFind/src/VuFind/Db/Row/Feedback.php rename to module/VuFind/src/VuFind/Db/Entity/Feedback.php index 960cbc621e9..8261a50a3c1 100644 --- a/module/VuFind/src/VuFind/Db/Row/Feedback.php +++ b/module/VuFind/src/VuFind/Db/Entity/Feedback.php @@ -1,11 +1,11 @@ - * @license https://opensource.org/licenses/gpl-2.0.php GNU General Public License - * @link https://vufind.org Main Site + * @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 */ -declare(strict_types=1); - -namespace VuFind\Db\Row; +namespace VuFind\Db\Entity; 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; +use Doctrine\ORM\Mapping as ORM; /** - * Class Feedback + * Entity model for feedback table * * @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 + * @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 * - * @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 + * @ORM\Table(name="feedback", + * indexes={@ORM\Index(name="created", columns={"created"}), + * @ORM\Index(name="status", columns={"status"}), + * @ORM\Index(name="form_name", columns={"form_name"})} + * ) + * @ORM\Entity */ -class Feedback extends RowGateway implements FeedbackEntityInterface, DbServiceAwareInterface +class Feedback implements FeedbackEntityInterface { - use DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false, + * options={"unsigned"=true} + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; /** - * Constructor + * Message + * + * @var string * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @ORM\Column(name="message", + * type="text", + * length=0, + * nullable=false + * ) */ - public function __construct($adapter) - { - parent::__construct('id', 'feedback', $adapter); - } + protected $message; + + /** + * Form data + * + * @var mixed + * + * @ORM\Column(name="form_data", + * type="json", + * length=0, + * nullable=true + * ) + */ + protected $formData; + + /** + * Form name + * + * @var string + * + * @ORM\Column(name="form_name", + * type="string", + * length=255, + * nullable=false + * ) + */ + protected $formName; + + /** + * Creation date + * + * @var DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $created = 'CURRENT_TIMESTAMP'; + + /** + * Last update date + * + * @var DateTime + * + * @ORM\Column(name="updated", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $updated = 'CURRENT_TIMESTAMP'; + + /** + * Status + * + * @var string + * + * @ORM\Column(name="status", + * type="string", + * length=255, + * nullable=false, + * options={"default"="open"} + * ) + */ + protected $status = 'open'; + + /** + * Site URL + * + * @var string + * + * @ORM\Column(name="site_url", + * type="string", + * length=255, + * nullable=false + * ) + */ + protected $siteUrl; + + /** + * User that created request + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * User that updated request + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="updated_by", + * referencedColumnName="id") + * }) + */ + protected $updatedBy; /** * Id getter @@ -108,13 +219,13 @@ public function getMessage(): string /** * Form data setter. * - * @param array $data Form data + * @param mixed $data Form data * * @return static */ public function setFormData(array $data): static { - $this->form_data = json_encode($data); + $this->formData = $data; return $this; } @@ -125,7 +236,7 @@ public function setFormData(array $data): static */ public function getFormData(): array { - return json_decode($this->form_data, true); + return $this->formData; } /** @@ -137,7 +248,7 @@ public function getFormData(): array */ public function setFormName(string $name): static { - $this->form_name = $name; + $this->formName = $name; return $this; } @@ -148,7 +259,7 @@ public function setFormName(string $name): static */ public function getFormName(): string { - return $this->form_name; + return $this->formName; } /** @@ -160,7 +271,7 @@ public function getFormName(): string */ public function setCreated(DateTime $dateTime): static { - $this->created = $dateTime->format('Y-m-d H:i:s'); + $this->created = $dateTime; return $this; } @@ -171,7 +282,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; } /** @@ -183,7 +294,7 @@ public function getCreated(): DateTime */ public function setUpdated(DateTime $dateTime): static { - $this->updated = $dateTime->format('Y-m-d H:i:s'); + $this->updated = $dateTime; return $this; } @@ -194,7 +305,7 @@ public function setUpdated(DateTime $dateTime): static */ public function getUpdated(): DateTime { - return DateTime::createFromFormat('Y-m-d H:i:s', $this->updated); + return $this->updated; } /** @@ -229,7 +340,7 @@ public function getStatus(): string */ public function setSiteUrl(string $url): static { - $this->site_url = $url; + $this->siteUrl = $url; return $this; } @@ -240,7 +351,7 @@ public function setSiteUrl(string $url): static */ public function getSiteUrl(): string { - return $this->site_url; + return $this->siteUrl; } /** @@ -252,7 +363,7 @@ public function getSiteUrl(): string */ public function setUser(?UserEntityInterface $user): static { - $this->user_id = $user?->getId(); + $this->user = $user; return $this; } @@ -263,9 +374,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; } /** @@ -277,7 +386,7 @@ public function getUser(): ?UserEntityInterface */ public function setUpdatedBy(?UserEntityInterface $user): static { - $this->updated_by = $user ? $user->getId() : null; + $this->updatedBy = $user; return $this; } @@ -288,8 +397,6 @@ public function setUpdatedBy(?UserEntityInterface $user): static */ public function getUpdatedBy(): ?UserEntityInterface { - return $this->updated_by - ? $this->getDbServiceManager()->get(UserServiceInterface::class)->getUserById($this->updated_by) - : null; + return $this->updatedBy; } } 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..cd44d6a4aaa 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; /** - * 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @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 + * @ORM\Table(name="login_token") + * @ORM\Entity */ -class LoginToken extends RowGateway implements DbServiceAwareInterface, LoginTokenEntityInterface +class LoginToken implements LoginTokenEntityInterface { - use DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Token. + * + * @var string + * + * @ORM\Column(name="token", + * type="string", + * length=255, + * nullable=false + * ) + */ + protected $token; /** - * Constructor + * Series. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var string + * + * @ORM\Column(name="series", + * type="string", + * length=255, + * nullable=false + * ) + */ + protected $series; + + /** + * Last login date. + * + * @var DateTime + * + * @ORM\Column(name="last_login", + * type="datetime", + * nullable=false + * ) + */ + protected $lastLogin; + + /** + * Browser. + * + * @var ?string + * + * @ORM\Column(name="browser", + * type="string", + * length=255, + * nullable=true + * ) + */ + protected $browser; + + /** + * Platform. + * + * @var ?string + * + * @ORM\Column(name="platform", + * type="string", + * length=255, + * nullable=true + * ) + */ + protected $platform; + + /** + * Expires. + * + * @var int + * + * @ORM\Column(name="expires", + * type="integer", + * nullable=false + * ) + */ + protected $expires; + + /** + * Last session ID. + * + * @var ?string + * + * @ORM\Column(name="last_session_id", + * type="string", + * length=255, + * nullable=true + * ) + */ + protected $lastSessionId; + + /** + * Constructor. */ - public function __construct($adapter) + public function __construct() { - parent::__construct('id', 'login_token', $adapter); + // Set the default value as a DateTime object + $this->lastLogin = DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00'); } /** @@ -88,7 +190,7 @@ public function getId(): int */ public function setUser(UserEntityInterface $user): static { - $this->user_id = $user->getId(); + $this->user = $user; return $this; } @@ -99,9 +201,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; } /** @@ -159,7 +259,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 +270,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 +351,7 @@ public function getExpires(): int */ public function setLastSessionId(?string $sid): static { - $this->last_session_id = $sid; + $this->lastSessionId = $sid; return $this; } @@ -262,6 +362,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/Row/OaiResumption.php b/module/VuFind/src/VuFind/Db/Entity/OaiResumption.php similarity index 53% rename from module/VuFind/src/VuFind/Db/Row/OaiResumption.php rename to module/VuFind/src/VuFind/Db/Entity/OaiResumption.php index fbd3e97a4f5..0bbbd5eef79 100644 --- a/module/VuFind/src/VuFind/Db/Row/OaiResumption.php +++ b/module/VuFind/src/VuFind/Db/Entity/OaiResumption.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\OaiResumptionEntityInterface; +use Doctrine\ORM\Mapping as ORM; /** - * Row Definition for oai_resumption + * OaiResumption * * @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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property string $params - * @property string $expires + * @ORM\Table(name="oai_resumption") + * @ORM\Entity */ -class OaiResumption extends RowGateway implements OaiResumptionEntityInterface +class OaiResumption implements OaiResumptionEntityInterface { /** - * 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") */ - public function __construct($adapter) - { - parent::__construct('id', 'oai_resumption', $adapter); - } + protected $id; /** - * Extract an array of parameters from the object. + * Resumption parameters. * - * @return array Original saved parameters. + * @var ?string * - * @deprecated Use parse_str() instead + * @ORM\Column(name="params", type="text", length=65535, nullable=true) */ - 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; - } + protected $params; /** - * Encode an array of parameters into the object. - * - * @param array $params Parameters to save. + * Expiry date. * - * @return void + * @var \DateTime * - * @deprecated Use \VuFind\Db\Service\OaiResumptionService::createAndPersistToken() + * @ORM\Column(name="expires", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) */ - public function saveParams($params) - { - ksort($params); - $processedParams = []; - foreach ($params as $key => $value) { - $processedParams[] = urlencode($key) . '=' . urlencode($value); - } - $this->params = implode('&', $processedParams); - } + protected $expires = '2000-01-01 00:00:00'; /** * Id getter @@ -138,7 +124,7 @@ public function getResumptionParameters(): ?string */ public function setExpiry(DateTime $dateTime): static { - $this->expires = $dateTime->format('Y-m-d H:i:s'); + $this->expires = $dateTime; return $this; } @@ -149,6 +135,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/PluginManager.php b/module/VuFind/src/VuFind/Db/Entity/PluginManager.php new file mode 100644 index 00000000000..d6bb0cd735d --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/PluginManager.php @@ -0,0 +1,119 @@ + + * @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; + } +} diff --git a/module/VuFind/src/VuFind/Db/Entity/Ratings.php b/module/VuFind/src/VuFind/Db/Entity/Ratings.php new file mode 100644 index 00000000000..d7d12ae5724 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Ratings.php @@ -0,0 +1,212 @@ + + * @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 ratings 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="ratings") + * @ORM\Entity + */ +class Ratings implements RatingsEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Resource ID. + * + * @var Resource + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\Resource") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="resource_id", + * referencedColumnName="id") + * }) + */ + protected $resource; + + /** + * Rating. + * + * @var int + * + * @ORM\Column(name="rating", type="integer", nullable=false) + */ + protected $rating; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * 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 resource. + * + * @return Resource + */ + public function getResource(): Resource + { + return $this->resource; + } + + /** + * Set resource. + * + * @param ResourceEntityInterface $resource Resource + * + * @return static + */ + public function setResource(ResourceEntityInterface $resource): static + { + $this->resource = $resource; + return $this; + } + + /** + * Get rating. + * + * @return int + */ + public function getRating(): int + { + return $this->rating; + } + + /** + * Set rating. + * + * @param int $rating Rating + * + * @return static + */ + public function setRating(int $rating): static + { + $this->rating = $rating; + 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; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/Record.php b/module/VuFind/src/VuFind/Db/Entity/Record.php similarity index 54% rename from module/VuFind/src/VuFind/Db/Row/Record.php rename to module/VuFind/src/VuFind/Db/Entity/Record.php index 1e3b897d0ae..2b9201f00db 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; /** - * 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property string $record_id - * @property string $source - * @property string $version - * @property string $updated + * @ORM\Table(name="record", + * uniqueConstraints={@ORM\UniqueConstraint(name="record_id_source", + * columns={"record_id", "source"})}) + * @ORM\Entity */ -class Record extends RowGateway implements RecordEntityInterface +class Record implements RecordEntityInterface { /** - * 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") */ - public function __construct($adapter) - { - parent::__construct('id', 'record', $adapter); - } + protected $id; + + /** + * Record ID. + * + * @var ?string + * + * @ORM\Column(name="record_id", type="string", length=255, nullable=true) + */ + protected $recordId; + + /** + * Record source. + * + * @var ?string + * + * @ORM\Column(name="source", type="string", length=50, nullable=true) + */ + protected $source; + + /** + * Record version. + * + * @var string + * + * @ORM\Column(name="version", type="string", length=20, nullable=false) + */ + protected $version; + + /** + * Record Data. + * + * @var ?string + * + * @ORM\Column(name="data", type="text", length=0, nullable=true) + */ + protected $data; + + /** + * Updated date. + * + * @var \DateTime + * + * @ORM\Column(name="updated", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $updated = '2000-01-01 00:00:00'; /** * Get identifier (returns null for an uninitialized or non-persisted object). @@ -67,7 +118,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -77,7 +128,7 @@ public function getId(): ?int */ public function getRecordId(): ?string { - return $this->record_id ?? null; + return $this->recordId; } /** @@ -89,7 +140,7 @@ public function getRecordId(): ?string */ public function setRecordId(?string $recordId): static { - $this->record_id = $recordId; + $this->recordId = $recordId; return $this; } @@ -100,19 +151,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 +174,7 @@ public function setSource(?string $recordSource): static */ public function getVersion(): string { - return $this->version ?? ''; + return $this->version; } /** @@ -146,11 +197,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 +209,7 @@ public function getData(): ?string */ public function setData(?string $recordData): static { - $this->__set('data', $recordData); + $this->data = $recordData; return $this; } @@ -173,7 +220,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 +232,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..2f4ab8c63f8 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Resource.php @@ -0,0 +1,254 @@ + + * @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", + * indexes={@ORM\Index(name="record_id", columns={"record_id"})} + * ) + * @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 $id; + + /** + * Record ID. + * + * @var string + * + * @ORM\Column(name="record_id", type="string", length=255, nullable=false) + */ + protected $recordId = ''; + + /** + * Record title. + * + * @var string + * + * @ORM\Column(name="title", type="string", length=255, nullable=false) + */ + protected $title = ''; + + /** + * Primary author. + * + * @var ?string + * + * @ORM\Column(name="author", type="string", length=255, nullable=true) + */ + protected $author; + + /** + * Published year. + * + * @var ?int + * + * @ORM\Column(name="year", type="integer", nullable=true) + */ + protected $year; + + /** + * Record source. + * + * @var string + * + * @ORM\Column(name="source", + * type="string", + * length=50, + * nullable=false, + * options={"default"="Solr"} + * ) + */ + protected $source = 'Solr'; + + /** + * Record Metadata + * + * @var ?string + * + * @ORM\Column(name="extra_metadata", + * type="text", + * length=16777215, + * nullable=true + * ) + */ + protected $extraMetadata; + + /** + * 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->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/Row/ResourceTags.php b/module/VuFind/src/VuFind/Db/Entity/ResourceTags.php similarity index 52% rename from module/VuFind/src/VuFind/Db/Row/ResourceTags.php rename to module/VuFind/src/VuFind/Db/Entity/ResourceTags.php index 783a212f8a7..8818efa34d9 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property int $resource_id - * @property int $tag_id - * @property int $list_id - * @property int $user_id - * @property string $posted + * @ORM\Table(name="resource_tags") + * @ORM\Entity */ -class ResourceTags extends RowGateway implements - ResourceTagsEntityInterface, - \VuFind\Db\Table\DbTableAwareInterface, - DbServiceAwareInterface +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 $id; /** - * Constructor + * Posted time. + * + * @var \DateTime + * + * @ORM\Column(name="posted", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $posted; + + /** + * Resource ID. + * + * @var Resource + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\Resource") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="resource_id", + * referencedColumnName="id") + * }) + */ + protected $resource; + + /** + * Tag ID. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var Tags + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\Tags") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="tag_id", + * referencedColumnName="id") + * }) + */ + protected $tag; + + /** + * List ID. + * + * @var UserList + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\UserList") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="list_id", + * referencedColumnName="id") + * }) + */ + protected $list; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * 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,19 +141,17 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** * Get resource. * - * @return ?ResourceEntityInterface + * @return ResourceEntityInterface */ - public function getResource(): ?ResourceEntityInterface + public function getResource(): ResourceEntityInterface { - return $this->resource_id - ? $this->getDbServiceManager()->get(ResourceServiceInterface::class)->getResourceById($this->resource_id) - : null; + return $this->resource; } /** @@ -107,7 +163,7 @@ public function getResource(): ?ResourceEntityInterface */ public function setResource(?ResourceEntityInterface $resource): static { - $this->resource_id = $resource?->getId(); + $this->resource = $resource; return $this; } @@ -118,9 +174,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 +186,7 @@ public function getTag(): TagsEntityInterface */ public function setTag(TagsEntityInterface $tag): static { - $this->tag_id = $tag->getId(); + $this->tag = $tag; return $this; } @@ -143,9 +197,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 +209,7 @@ public function getUserList(): ?UserListEntityInterface */ public function setUserList(?UserListEntityInterface $list): static { - $this->list_id = $list?->getId(); + $this->list = $list; return $this; } @@ -168,21 +220,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 +243,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 +255,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..26d806fbb65 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Search.php @@ -0,0 +1,412 @@ + + * @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; + +/** + * 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", columns={"notification_base_url"}), + * @ORM\Index(name="notification_frequency", columns={"notification_frequency"}), + * @ORM\Index(name="session_id", columns={"session_id"}), + * @ORM\Index(name="user_id", columns={"user_id"})}) + * @ORM\Entity + */ +class Search implements SearchEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="bigint", + * nullable=false, + * options={"unsigned"=true} + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Session ID. + * + * @var ?string + * + * @ORM\Column(name="session_id", type="string", length=128, nullable=true) + */ + protected $sessionId; + + /** + * Created date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * Title. + * + * @var ?string + * + * @ORM\Column(name="title", type="string", length=20, nullable=true) + */ + protected $title; + + /** + * Saved. + * + * @var bool + * + * @ORM\Column(name="saved", type="boolean", nullable=false) + */ + protected $saved = '0'; + + /** + * Search object. + * + * @var string + * + * @ORM\Column(name="search_object", type="blob", length=65535, nullable=true) + */ + protected $searchObject; + + /** + * Checksum + * + * @var ?int + * + * @ORM\Column(name="checksum", type="integer", nullable=true) + */ + protected $checksum; + + /** + * Notification frequency. + * + * @var int + * + * @ORM\Column(name="notification_frequency", type="integer", nullable=false) + */ + protected $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 $lastNotificationSent = '2000-01-01 00:00:00'; + + /** + * Notification base URL. + * + * @var string + * + * @ORM\Column(name="notification_base_url", + * type="string", + * length=255, nullable=false + * ) + */ + protected $notificationBaseUrl = ''; + + /** + * 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 (bool)$this->saved; + } + + /** + * Set saved. + * + * @param bool $saved Saved + * + * @return static + */ + public function setSaved(bool $saved): static + { + $this->saved = $saved ? '1' : '0'; + return $this; + } + + /** + * Get the search object from the row. + * + * @return ?\VuFind\Search\Minified + */ + public function getSearchObject(): ?\VuFind\Search\Minified + { + return $this->searchObject ? unserialize($this->searchObject) : null; + } + + /** + * 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; + 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/Entity/Session.php b/module/VuFind/src/VuFind/Db/Entity/Session.php new file mode 100644 index 00000000000..a58860e2c2b --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Session.php @@ -0,0 +1,188 @@ + + * @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; + +/** + * Session + * + * @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="session", + * uniqueConstraints={@ORM\UniqueConstraint(name="session_id", + * columns={"session_id"})}, + * indexes={@ORM\Index(name="last_used", columns={"last_used"})}) + * @ORM\Entity + */ +class Session implements SessionEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="bigint", + * nullable=false, + * options={"unsigned"=true} + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Session ID. + * + * @var ?string + * + * @ORM\Column(name="session_id", type="string", length=128, nullable=true) + */ + protected $sessionId; + + /** + * Session data. + * + * @var ?string + * + * @ORM\Column(name="data", type="text", length=16777215, nullable=true) + */ + protected $data; + + /** + * Time session last used. + * + * @var int + * + * @ORM\Column(name="last_used", type="integer", nullable=false) + */ + protected $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 $created = '2000-01-01 00:00:00'; + + /** + * Id getter + * + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * Session Id setter + * + * @param ?string $sid Session Id. + * + * @return static + */ + public function setSessionId(?string $sid): static + { + $this->sessionId = $sid; + return $this; + } + + /** + * Created setter. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setCreated(DateTime $dateTime): static + { + $this->created = $dateTime; + return $this; + } + + /** + * Set time the session is last used. + * + * @param int $lastused Time last used + * + * @return static + */ + public function setLastUsed(int $lastused): static + { + $this->lastUsed = $lastused; + return $this; + } + + /** + * Get time when the session was last used. + * + * @return int + */ + public function getLastUsed(): int + { + return $this->lastUsed; + } + + /** + * Session data setter. + * + * @param ?string $data Session data. + * + * @return static + */ + public function setData(?string $data): static + { + $this->data = $data; + return $this; + } + + /** + * Get session data. + * + * @return ?string + */ + public function getData(): ?string + { + return $this->data; + } +} diff --git a/module/VuFind/src/VuFind/Db/Row/Shortlinks.php b/module/VuFind/src/VuFind/Db/Entity/Shortlinks.php similarity index 58% rename from module/VuFind/src/VuFind/Db/Row/Shortlinks.php rename to module/VuFind/src/VuFind/Db/Entity/Shortlinks.php index 13d1a716a24..aa7889e32c8 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @property int $id - * @property string $path - * @property string $hash - * @property string $created + * @ORM\Entity + * @ORM\Table(name="shortlinks", + * uniqueConstraints={@ORM\UniqueConstraint(name="shortlinks_hash_IDX", + * columns={"hash"})} + * ) */ -class Shortlinks extends RowGateway implements ShortlinksEntityInterface +class Shortlinks implements ShortlinksEntityInterface { /** - * Constructor + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Path (minus hostname) from shortened URL. + * + * @var string + * + * @ORM\Column(name="path", type="text", length=16777215, nullable=false) + */ + protected $path; + + /** + * Shortlinks hash. * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @var ?string + * + * @ORM\Column(name="hash", type="string", length=32, nullable=true) + */ + protected $hash; + + /** + * Creation timestamp. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $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(); } /** @@ -65,7 +110,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -75,7 +120,7 @@ public function getId(): ?int */ public function getPath(): string { - return $this->path ?? ''; + return $this->path; } /** @@ -99,7 +144,7 @@ public function setPath(string $path): static */ public function getHash(): ?string { - return $this->hash ?? null; + return $this->hash; } /** @@ -122,7 +167,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 +179,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..de289977b88 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/Tags.php @@ -0,0 +1,103 @@ + + * @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 $id; + + /** + * Name of tag. + * + * @var string + * + * @ORM\Column(name="tag", type="string", length=64, nullable=false) + */ + protected $tag = ''; + + /** + * 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/Entity/User.php b/module/VuFind/src/VuFind/Db/Entity/User.php new file mode 100644 index 00000000000..69e40f81d38 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/User.php @@ -0,0 +1,764 @@ + + * @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; + +/** + * 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`", + * uniqueConstraints={@ORM\UniqueConstraint(name="cat_id", + * columns={"cat_id"}), + * @ORM\UniqueConstraint(name="username", columns={"username"})}) + * @ORM\Entity + */ +class User implements UserEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Username + * + * @var string + * + * @ORM\Column(name="username", type="string", length=255, nullable=false) + */ + protected $username = ''; + + /** + * Password + * + * @var string + * + * @ORM\Column(name="password", type="string", length=32, nullable=false) + */ + protected $password = ''; + + /** + * Hash of the password. + * + * @var ?string + * + * @ORM\Column(name="pass_hash", type="string", length=60, nullable=true) + */ + protected $passHash; + + /** + * First Name. + * + * @var string + * + * @ORM\Column(name="firstname", type="string", length=50, nullable=false) + */ + protected $firstname = ''; + + /** + * Last Name. + * + * @var string + * + * @ORM\Column(name="lastname", type="string", length=50, nullable=false) + */ + protected $lastname = ''; + + /** + * Email. + * + * @var string + * + * @ORM\Column(name="email", type="string", length=255, nullable=false) + */ + protected $email = ''; + + /** + * Date of email verification. + * + * @var ?DateTime + * + * @ORM\Column(name="email_verified", type="datetime", nullable=true) + */ + protected $emailVerified; + + /** + * Pending email. + * + * @var string + * + * @ORM\Column(name="pending_email", type="string", length=255, nullable=false) + */ + protected $pendingEmail = ''; + + /** + * User provided email. + * + * @var bool + * + * @ORM\Column(name="user_provided_email", type="boolean", nullable=false) + */ + protected $userProvidedEmail = '0'; + + /** + * Cat ID. + * + * @var ?string + * + * @ORM\Column(name="cat_id", type="string", length=255, nullable=true) + */ + protected $catId; + + /** + * Cat username. + * + * @var ?string + * + * @ORM\Column(name="cat_username", type="string", length=50, nullable=true) + */ + protected $catUsername; + + /** + * Cat password. + * + * @var ?string + * + * @ORM\Column(name="cat_password", type="string", length=70, nullable=true) + */ + protected $catPassword; + + /** + * Cat encrypted password. + * + * @var ?string + * + * @ORM\Column(name="cat_pass_enc", type="string", length=255, nullable=true) + */ + protected $catPassEnc; + + /** + * College. + * + * @var string + * + * @ORM\Column(name="college", type="string", length=100, nullable=false) + */ + protected $college = ''; + + /** + * Major. + * + * @var string + * + * @ORM\Column(name="major", type="string", length=100, nullable=false) + */ + protected $major = ''; + + /** + * Home library. + * + * @var string + * + * @ORM\Column(name="home_library", type="string", length=100, nullable=true) + */ + protected $homeLibrary = ''; + + /** + * Creation date. + * + * @var DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created; + + /** + * Verify hash. + * + * @var string + * + * @ORM\Column(name="verify_hash", type="string", length=42, nullable=false) + */ + protected $verifyHash = ''; + + /** + * Time last loggedin. + * + * @var DateTime + * + * @ORM\Column(name="last_login", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $lastLogin; + + /** + * Method of authentication. + * + * @var ?string + * + * @ORM\Column(name="auth_method", type="string", length=50, nullable=true) + */ + protected $authMethod; + + /** + * Last known language. + * + * @var string + * + * @ORM\Column(name="last_language", type="string", length=30, nullable=false) + */ + protected $lastLanguage = ''; + + /** + * Constructor + */ + public function __construct() + { + // Set the default values as \DateTime objects + $this->created = $this->lastLogin = DateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00'); + } + + /** + * 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->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 (bool)$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 ? '1' : '0'; + return $this; + } + + /** + * Last login setter. + * + * @param DateTime $dateTime Last login date + * + * @return static + */ + public function setLastLogin(DateTime $dateTime): static + { + $this->lastLogin = $dateTime; + return $this; + } + + /** + * Last login getter + * + * @return DateTime + */ + public function getLastLogin(): DateTime + { + return $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; + } +} 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..1e596955841 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; /** - * 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 + * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki * - * @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 + * @ORM\Table(name="user_card", + * indexes={@ORM\Index(name="user_card_cat_username", columns={"cat_username"}), + * @ORM\Index(name="user_id", columns={"user_id"})}) + * @ORM\Entity */ -class UserCard extends RowGateway implements DbServiceAwareInterface, UserCardEntityInterface +class UserCard implements UserCardEntityInterface { - use \VuFind\Db\Service\DbServiceAwareTrait; + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; /** - * Constructor + * Card name. + * + * @var string * - * @param \Laminas\Db\Adapter\Adapter $adapter Database adapter + * @ORM\Column(name="card_name", type="string", length=255, nullable=false) + */ + protected $cardName = ''; + + /** + * Cat username. + * + * @var string + * + * @ORM\Column(name="cat_username", type="string", length=50, nullable=false) + */ + protected $catUsername = ''; + + /** + * Cat password. + * + * @var ?string + * + * @ORM\Column(name="cat_password", type="string", length=70, nullable=true) + */ + protected $catPassword; + + /** + * Cat password (encrypted). + * + * @var ?string + * + * @ORM\Column(name="cat_pass_enc", type="string", length=255, nullable=true) + */ + protected $catPassEnc; + + /** + * Home library. + * + * @var string + * + * @ORM\Column(name="home_library", type="string", length=100, nullable=true) + */ + protected $homeLibrary = ''; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created; + + /** + * Saved timestamp. + * + * @var \DateTime + * + * @ORM\Column(name="saved", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"} + * ) + */ + protected $saved; + + /** + * User. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $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 = new \DateTime('2000-01-01 00:00:00'); + $this->saved = new \DateTime(); } /** @@ -74,7 +163,7 @@ public function __construct($adapter) */ public function getId(): ?int { - return $this->id ?? null; + return $this->id; } /** @@ -86,7 +175,7 @@ public function getId(): ?int */ public function setCardName(string $cardName): static { - $this->card_name = $cardName; + $this->cardName = $cardName; return $this; } @@ -97,7 +186,7 @@ public function setCardName(string $cardName): static */ public function getCardName(): string { - return $this->card_name; + return $this->cardName; } /** @@ -109,7 +198,7 @@ public function getCardName(): string */ public function setCatUsername(string $catUsername): static { - $this->cat_username = $catUsername; + $this->catUsername = $catUsername; return $this; } @@ -120,7 +209,7 @@ public function setCatUsername(string $catUsername): static */ public function getCatUsername(): string { - return $this->cat_username; + return $this->catUsername; } /** @@ -132,7 +221,7 @@ public function getCatUsername(): string */ public function setRawCatPassword(?string $catPassword): static { - $this->cat_password = $catPassword; + $this->catPassword = $catPassword; return $this; } @@ -143,7 +232,7 @@ public function setRawCatPassword(?string $catPassword): static */ public function getRawCatPassword(): ?string { - return $this->cat_password; + return $this->catPassword; } /** @@ -155,7 +244,7 @@ public function getRawCatPassword(): ?string */ public function setCatPassEnc(?string $passEnc): static { - $this->cat_pass_enc = $passEnc; + $this->catPassEnc = $passEnc; return $this; } @@ -166,7 +255,7 @@ public function setCatPassEnc(?string $passEnc): static */ public function getCatPassEnc(): ?string { - return $this->cat_pass_enc; + return $this->catPassEnc; } /** @@ -178,7 +267,7 @@ public function getCatPassEnc(): ?string */ public function setHomeLibrary(?string $homeLibrary): static { - $this->home_library = $homeLibrary; + $this->homeLibrary = $homeLibrary; return $this; } @@ -189,7 +278,7 @@ public function setHomeLibrary(?string $homeLibrary): static */ public function getHomeLibrary(): ?string { - return $this->home_library; + return $this->homeLibrary; } /** @@ -201,7 +290,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 +301,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 +313,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 +324,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 +336,7 @@ public function getSaved(): DateTime */ public function setUser(UserEntityInterface $user): static { - $this->user_id = $user->getId(); + $this->user = $user; return $this; } @@ -258,6 +347,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/UserList.php b/module/VuFind/src/VuFind/Db/Entity/UserList.php new file mode 100644 index 00000000000..d744c352ce7 --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/UserList.php @@ -0,0 +1,242 @@ + + * @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; + +/** + * 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", + * indexes={@ORM\Index(name="user_id", columns={"user_id"})} + * ) + * @ORM\Entity + */ +class UserList implements UserListEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Id + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Title of the list. + * + * @var string + * + * @ORM\Column(name="title", type="string", length=200, nullable=false) + */ + protected $title = ''; + + /** + * Description of the list. + * + * @var ?string + * + * @ORM\Column(name="description", type="text", length=65535, nullable=true) + */ + protected $description; + + /** + * Creation date. + * + * @var \DateTime + * + * @ORM\Column(name="created", + * type="datetime", + * nullable=false, + * options={"default"="2000-01-01 00:00:00"} + * ) + */ + protected $created = '2000-01-01 00:00:00'; + + /** + * Flag to indicate whether or not the list is public. + * + * @var bool + * + * @ORM\Column(name="public", type="boolean", nullable=false) + */ + protected $public = false; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id" + * )}) + */ + protected $user; + + /** + * Get identifier (returns null for an uninitialized or non-persisted object). + * + * @return ?int + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * 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 (bool)($this->public ?? false); + } + + /** + * 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/UserResource.php b/module/VuFind/src/VuFind/Db/Entity/UserResource.php new file mode 100644 index 00000000000..0173778057c --- /dev/null +++ b/module/VuFind/src/VuFind/Db/Entity/UserResource.php @@ -0,0 +1,260 @@ + + * @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; + +/** + * UserResource + * + * @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_resource", + * indexes={@ORM\Index(name="list_id", columns={"list_id"}), + * @ORM\Index(name="resource_id", columns={"resource_id"}), + * @ORM\Index(name="user_id", columns={"user_id"})} + * ) + * @ORM\Entity + */ +class UserResource implements UserResourceEntityInterface +{ + /** + * Unique ID. + * + * @var int + * + * @ORM\Column(name="id", + * type="integer", + * nullable=false + * ) + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + */ + protected $id; + + /** + * Notes associated with the resource. + * + * @var ?string + * + * @ORM\Column(name="notes", type="text", length=65535, nullable=true) + */ + protected $notes; + + /** + * Date saved. + * + * @var \DateTime + * + * @ORM\Column(name="saved", + * type="datetime", + * nullable=false, + * options={"default"="CURRENT_TIMESTAMP"}) + */ + protected $saved; + + /** + * User ID. + * + * @var User + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\User") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="user_id", + * referencedColumnName="id") + * }) + */ + protected $user; + + /** + * Resource. + * + * @var Resource + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\Resource") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="resource_id", + * referencedColumnName="id") + * }) + */ + protected $resource; + + /** + * User list ID. + * + * @var UserList + * + * @ORM\ManyToOne(targetEntity="VuFind\Db\Entity\UserList") + * @ORM\JoinColumns({ + * @ORM\JoinColumn(name="list_id", + * referencedColumnName="id") + * }) + */ + protected $list; + + /** + * Constructor + */ + public function __construct() + { + // Set the default value as a \DateTime object + $this->saved = new \DateTime(); + } + + /** + * 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 resource. + * + * @return ResourceEntityInterface + */ + public function getResource(): ResourceEntityInterface + { + return $this->resource; + } + + /** + * Set resource. + * + * @param ResourceEntityInterface $resource Resource + * + * @return static + */ + public function setResource(ResourceEntityInterface $resource): static + { + $this->resource = $resource; + return $this; + } + + /** + * Get user list. + * + * @return UserListEntityInterface + */ + public function getUserList(): UserListEntityInterface + { + return $this->list; + } + + /** + * Set user list. + * + * @param ?UserListEntityInterface $list User List + * + * @return static + */ + public function setUserList(?UserListEntityInterface $list): static + { + $this->list = $list; + return $this; + } + + /** + * Get notes. + * + * @return ?string + */ + public function getNotes(): ?string + { + return $this->notes; + } + + /** + * Set notes. + * + * @param ?string $notes Notes associated with the resource + * + * @return static + */ + public function setNotes(?string $notes): static + { + $this->notes = $notes; + return $this; + } + + /** + * Get saved date. + * + * @return DateTime + */ + public function getSaved(): DateTime + { + return $this->saved; + } + + /** + * Set saved date. + * + * @param DateTime $dateTime Created date + * + * @return static + */ + public function setSaved(DateTime $dateTime): static + { + $this->saved = $dateTime; + return $this; + } +} 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/PluginManager.php b/module/VuFind/src/VuFind/Db/Row/PluginManager.php index 5f15b673e6f..3e8a81f144a 100644 --- a/module/VuFind/src/VuFind/Db/Row/PluginManager.php +++ b/module/VuFind/src/VuFind/Db/Row/PluginManager.php @@ -46,23 +46,11 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @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, ]; @@ -72,25 +60,11 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @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, ]; 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/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 index c45496bc880..a6c701a4679 100644 --- a/module/VuFind/src/VuFind/Db/Row/User.php +++ b/module/VuFind/src/VuFind/Db/Row/User.php @@ -5,7 +5,7 @@ * * PHP version 8 * - * Copyright (C) Villanova University 2010. + * Copyright (C) Villanova University 2023. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -30,15 +30,11 @@ 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\UserCard; 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; @@ -85,8 +81,8 @@ class User extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface, \LmcRbacMvc\Identity\IdentityInterface { - use \VuFind\Db\Service\DbServiceAwareTrait; use \VuFind\Db\Table\DbTableAwareTrait; + use \VuFind\Db\Service\DbServiceAwareTrait; /** * VuFind configuration @@ -281,58 +277,6 @@ 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. * @@ -357,47 +301,6 @@ public function formatTagString($tags) 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. * @@ -419,71 +322,6 @@ public function getSavedData( 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 * @@ -499,7 +337,7 @@ public function libraryCardsEnabled() /** * Get all library cards associated with the user. * - * @return \Laminas\Db\ResultSet\AbstractResultSet + * @return array * @throws \VuFind\Exception\LibraryCard * * @deprecated Use UserCardServiceInterface::getLibraryCards() @@ -509,8 +347,7 @@ public function getLibraryCards() if (!$this->capabilities->libraryCardsEnabled()) { return new \Laminas\Db\ResultSet\ResultSet(); } - $userCard = $this->getDbTable('UserCard'); - return $userCard->select(['user_id' => $this->id]); + return $this->getUserCardService()->getLibraryCards($this->id); } /** 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 fd77263ce60..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 5bed4d2c2e8..263e2b7132b 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 Doctrine\ORM\EntityManager; use Laminas\Db\RowGateway\AbstractRowGateway; use VuFind\Db\Entity\EntityInterface; +use VuFind\Db\Entity\PluginManager as EntityPluginManager; + +use function is_callable; +use function is_int; /** * Database service abstract base class @@ -43,6 +48,31 @@ */ abstract class AbstractDbService implements DbServiceInterface { + /** + * Constructor + * + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager VuFind entity plugin manager + */ + public function __construct( + protected EntityManager $entityManager, + protected EntityPluginManager $entityPluginManager + ) { + } + + /** + * Resolve an entity class name using the plugin manager. + * + * @param string $entity Entity class name or alias + * + * @return string + */ + protected function getEntityClass(string $entity): string + { + $entity = $this->entityPluginManager->get($entity); + return $entity::class; + } + /** * Persist an entity. * @@ -52,9 +82,82 @@ abstract class AbstractDbService implements DbServiceInterface */ public function persistEntity(EntityInterface $entity): void { - if (!$entity instanceof AbstractRowGateway) { - throw new \Exception('Unexpected entity type'); + // Compatibility with legacy \VuFind\Db\Row objects: + if ($entity instanceof AbstractRowGateway) { + $entity->save(); + return; + } + $this->entityManager->persist($entity); + $this->entityManager->flush(); + } + + /** + * Delete an entity. + * + * @param EntityInterface $entity Entity to persist. + * + * @return void + */ + public function deleteEntity(EntityInterface $entity): void + { + $this->entityManager->remove($entity); + $this->entityManager->flush(); + } + + /** + * 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( + $this->getEntityClass($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 ' . $this->getEntityClass($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 21e3cc9bbee..d3d513a4590 100644 --- a/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php +++ b/module/VuFind/src/VuFind/Db/Service/AbstractDbServiceFactory.php @@ -65,6 +65,10 @@ 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), + ...($options ?? []) + ); } } diff --git a/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php b/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php index 7eebfae3786..81f593eeaf5 100644 --- a/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php +++ b/module/VuFind/src/VuFind/Db/Service/AccessTokenService.php @@ -30,10 +30,9 @@ namespace VuFind\Db\Service; use DateTime; -use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\AccessToken; 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 +45,17 @@ */ 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 { + $class = $this->getEntityClass(AccessToken::class); + return new $class(); } /** @@ -75,7 +73,22 @@ public function getByIdAndType( string $type, bool $create = true ): ?AccessTokenEntityInterface { - return $this->accessTokenTable->getByIdAndType($id, $type, $create); + $dql = 'SELECT at ' + . 'FROM ' . $this->getEntityClass(AccessToken::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 +101,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 +117,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 +132,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($this->getEntityClass(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($this->getEntityClass(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 12efff936d3..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..da9ff8af280 100644 --- a/module/VuFind/src/VuFind/Db/Service/AuthHashService.php +++ b/module/VuFind/src/VuFind/Db/Service/AuthHashService.php @@ -30,9 +30,8 @@ namespace VuFind\Db\Service; use DateTime; +use VuFind\Db\Entity\AuthHash; use VuFind\Db\Entity\AuthHashEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for auth_hash table. @@ -45,11 +44,8 @@ */ class AuthHashService extends AbstractDbService implements AuthHashServiceInterface, - DbTableAwareInterface, Feature\DeleteExpiredInterface { - use DbTableAwareTrait; - /** * Create an auth_hash entity object. * @@ -57,7 +53,8 @@ class AuthHashService extends AbstractDbService implements */ public function createEntity(): AuthHashEntityInterface { - return $this->getDbTable('AuthHash')->createRow(); + $class = $this->getEntityClass(AuthHash::class); + return new $class(); } /** @@ -69,8 +66,12 @@ public function createEntity(): AuthHashEntityInterface */ public function deleteAuthHash(AuthHashEntityInterface|int $authHashOrId): void { + $dql = 'DELETE FROM ' . $this->getEntityClass(AuthHash::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 +86,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 ' . $this->getEntityClass(AuthHash::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 +113,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 ' . $this->getEntityClass(AuthHash::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 +133,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($this->getEntityClass(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($this->getEntityClass(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..8895295d2db 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 ' . $this->getEntityClass(ChangeTracker::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 ' . $this->getEntityClass(ChangeTracker::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,48 @@ 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 ' . $this->getEntityClass(ChangeTracker::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|false + */ + public function retrieveOrCreate(string $core, string $id): ChangeTracker|false + { + $row = $this->getChangeTrackerEntity($core, $id); + if (empty($row)) { + $now = new \DateTime('now', new \DateTimeZone('UTC')); + $row = $this->createEntity() + ->setIndexName($core) + ->setId($id) + ->setFirstIndexed($now) + ->setLastIndexed($now); + try { + $this->persistEntity($row); + } catch (\Exception $e) { + $this->logError('Could not save change tracker record: ' . $e->getMessage()); + return false; + } + } + return $row; } /** @@ -131,7 +167,23 @@ 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('now', new \DateTimeZone('UTC'))); + try { + $this->persistEntity($row); + } catch (\Exception $e) { + $this->logError('Could not update the deleted time: ' . $e->getMessage()); + return false; + } + return $row; } /** @@ -150,6 +202,86 @@ 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, new \DateTimeZone('UTC')); + + // 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('now', new \DateTimeZone('UTC')); + $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 ' . $this->getEntityClass(ChangeTracker::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 ChangeTracker + */ + public function createEntity(): ChangeTracker + { + $class = $this->getEntityClass(ChangeTracker::class); + return new $class(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/CommentsService.php b/module/VuFind/src/VuFind/Db/Service/CommentsService.php index a922986f5b7..36a28956db9 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 Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\Comments; use VuFind\Db\Entity\CommentsEntityInterface; +use VuFind\Db\Entity\Resource; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\User; 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,8 @@ class CommentsService extends AbstractDbService implements */ public function createEntity(): CommentsEntityInterface { - return $this->getDbTable('comments')->createRow(); + $class = $this->getEntityClass(Comments::class); + return new $class(); } /** @@ -79,13 +82,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(User::class, $userOrId)) + ->setComment($comment) + ->setCreated(new \DateTime()) + ->setResource($this->getDoctrineReference(Resource::class, $resourceOrId)); + + try { + $this->persistEntity($data); + } catch (\Exception $e) { + $this->logError('Could not save comment: ' . $e->getMessage()); + return null; + } + + return $data->getId(); } /** @@ -98,8 +108,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 ' . $this->getEntityClass(Comments::class) . ' c ' + . 'LEFT JOIN c.user u ' + . 'WHERE c.resource = :resource ' + . 'ORDER BY c.created'; + + $parameters = compact('resource'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->getResult(); + return $result; } /** @@ -112,9 +136,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 ' . $this->getEntityClass(Comments::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 +163,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 ' . $this->getEntityClass(Comments::class) . ' c ' + . 'WHERE c.user = :user'; + $query = $this->entityManager->createQuery($dql); + $query->setParameters(['user' => is_int($userOrId) ? $userOrId : $userOrId->getId()]); + $query->execute(); } /** @@ -138,7 +177,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 ' . $this->getEntityClass(Comments::class) . ' c'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -150,7 +195,10 @@ public function getStatistics(): array */ public function getCommentById(int $id): ?CommentsEntityInterface { - return $this->getDbTable('comments')->select(['id' => $id])->current(); + return $this->entityManager->find( + $this->getEntityClass(Comments::class), + $id + ); } /** @@ -163,6 +211,11 @@ 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 ' . $this->getEntityClass(Comments::class) . ' e ' + . 'SET e.resource = :new WHERE e.resource = :old'; + $parameters = compact('new', 'old'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/FeedbackService.php b/module/VuFind/src/VuFind/Db/Service/FeedbackService.php index 038d953946a..8625df1a099 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,8 @@ class FeedbackService extends AbstractDbService implements DbTableAwareInterface */ public function createEntity(): FeedbackEntityInterface { - return $this->getDbTable('feedback')->createRow(); + $class = $this->getEntityClass(Feedback::class); + return new $class(); } /** @@ -70,7 +82,7 @@ public function createEntity(): FeedbackEntityInterface */ public function getFeedbackById(int $id): ?FeedbackEntityInterface { - return $this->getDbTable('feedback')->select(['id' => $id])->current(); + return $this->entityManager->find($this->getEntityClass(Feedback::class), $id); } /** @@ -91,25 +103,43 @@ 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, CONCAT(u.firstname, ' ', u.lastname) AS user_name, " + . "CONCAT(m.firstname, ' ', m.lastname) AS manager_name " + . 'FROM ' . $this->getEntityClass(Feedback::class) . ' f ' + . 'LEFT JOIN f.user u ' + . 'LEFT JOIN f.updatedBy m'; + $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 +151,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 ' . $this->getEntityClass(Feedback::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 ' . $this->getEntityClass(Feedback::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 +200,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..8b33c103ed2 100644 --- a/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php +++ b/module/VuFind/src/VuFind/Db/Service/LoginTokenService.php @@ -30,13 +30,12 @@ namespace VuFind\Db\Service; use DateTime; +use VuFind\Db\Entity\LoginToken; use VuFind\Db\Entity\LoginTokenEntityInterface; +use VuFind\Db\Entity\User; 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 +47,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 +56,8 @@ class LoginTokenService extends AbstractDbService implements */ public function createEntity(): LoginTokenEntityInterface { - return $this->getDbTable('LoginToken')->createRow(); + $class = $this->getEntityClass(LoginToken::class); + return new $class(); } /** @@ -108,7 +105,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 ' . $this->getEntityClass(LoginToken::class) . ' lt ' + . 'WHERE lt.id == :id'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('id', $id); + $query->execute(); } /** @@ -121,7 +148,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 ' . $this->getEntityClass(LoginToken::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 +169,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(User::class, $userOrId); + $dql = 'DELETE FROM ' . $this->getEntityClass(LoginToken::class) . ' lt ' + . 'WHERE lt.user = :user'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('user', $user); + $query->execute(); } /** @@ -147,8 +187,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(User::class, $userOrId); + if ($grouped) { + // Use different DQL for grouping logic + $dql = 'SELECT lt ' + . 'FROM ' . $this->getEntityClass(LoginTokenEntityInterface::class) . ' lt ' + . 'WHERE lt.user = :user AND lt.lastLogin = (' + . ' SELECT MAX(subLt.lastLogin) ' + . ' FROM ' . $this->getEntityClass(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 ' . $this->getEntityClass(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 +221,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 ' . $this->getEntityClass(LoginTokenEntityInterface::class) . ' lt ' + . 'WHERE lt.series = :series'; + $query = $this->entityManager->createQuery($dql); + $query->setParameter('series', $series); + $result = $query->getResult(); + return $result; } /** @@ -173,6 +240,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($this->getEntityClass(LoginTokenEntityInterface::class), 'lt') + ->where('lt.expires < :dateLimit') + ->setParameter('dateLimit', $dateLimit->getTimestamp()); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete($this->getEntityClass(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 a1c00678218..024bffa7abf 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; /** @@ -45,11 +44,9 @@ * @link https://vufind.org/wiki/development:plugins:database_gateways Wiki */ class OaiResumptionService extends AbstractDbService implements - DbTableAwareInterface, LoggerAwareInterface, OaiResumptionServiceInterface { - use DbTableAwareTrait; use LoggerAwareTrait; /** @@ -59,7 +56,12 @@ class OaiResumptionService extends AbstractDbService implements */ public function removeExpired(): void { - $this->getDbTable('oairesumption')->removeExpired(); + $dql = 'DELETE FROM ' . $this->getEntityClass(OaiResumption::class) . ' O ' + . 'WHERE O.expires <= :now'; + $parameters['now'] = new \DateTime(); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -70,9 +72,16 @@ public function removeExpired(): void * * @return ?OaiResumptionEntityInterface */ - public function findToken(string $token): ?OaiResumptionEntityInterface + public function findToken($token): ?OaiResumptionEntityInterface { - return $this->getDbTable('oairesumption')->findToken($token); + $dql = 'SELECT O ' + . 'FROM ' . $this->getEntityClass(OaiResumption::class) . ' O ' + . 'WHERE O.id = :token'; + $parameters = compact('token'); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $records = $query->getResult(); + return current($records); } /** @@ -105,7 +114,8 @@ public function createAndPersistToken(array $params, int $expire): OaiResumption */ public function createEntity(): OaiResumptionEntityInterface { - return $this->getDbTable('oairesumption')->createRow(); + $class = $this->getEntityClass(OaiResumption::class); + return new $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 4c19ddef4b6..8a26f671374 100644 --- a/module/VuFind/src/VuFind/Db/Service/RatingsService.php +++ b/module/VuFind/src/VuFind/Db/Service/RatingsService.php @@ -29,10 +29,13 @@ namespace VuFind\Db\Service; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\Ratings; +use VuFind\Db\Entity\Resource; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\User; 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 +49,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 +67,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 ' . $this->getEntityClass(Ratings::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, + ]; } /** @@ -80,7 +109,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 ' . $this->getEntityClass(Ratings::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 +166,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 ' . $this->getEntityClass(Ratings::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 +181,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 ' . $this->getEntityClass(Ratings::class) . ' r'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -122,8 +205,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 ' . $this->getEntityClass(Ratings::class) . ' r ' + . 'WHERE r.user = :user AND r.resource = :resource'; + $resource = $this->getDoctrineReference(Resource::class, $resourceOrId); + $user = $this->getDoctrineReference(User::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->createRatings() + ->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 Ratings + */ + public function createRatings(): Ratings + { + $class = $this->getEntityClass(Ratings::class); + return new $class(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/RecordService.php b/module/VuFind/src/VuFind/Db/Service/RecordService.php index 1eff27251d5..03effd8aa2c 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\Resource; +use VuFind\Db\Entity\UserResource; + +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 ' . $this->getEntityClass(Record::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 ' . $this->getEntityClass(Record::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 ' . $this->getEntityClass(Record::class) . ' r ' + . 'JOIN ' . $this->getEntityClass(Resource::class) . ' re ' + . 'WITH r.recordId = re.recordId AND r.source = re.source ' + . 'LEFT JOIN ' . $this->getEntityClass(UserResource::class) . ' ur ' + . 'WITH re.id = ur.resource ' + . 'WHERE ur.id IS NULL'; + $query = $this->entityManager->createQuery($dql); + $ids = $query->getResult(); + $dql = 'DELETE FROM ' . $this->getEntityClass(Record::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 ' . $this->getEntityClass(Record::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,7 @@ public function deleteRecord(string $id, string $source): bool */ public function createEntity(): RecordEntityInterface { - return $this->getDbTable('record')->createRow(); + $class = $this->getEntityClass(Record::class); + return new $class(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ResourceService.php b/module/VuFind/src/VuFind/Db/Service/ResourceService.php index 6317a454235..090295076cc 100644 --- a/module/VuFind/src/VuFind/Db/Service/ResourceService.php +++ b/module/VuFind/src/VuFind/Db/Service/ResourceService.php @@ -30,13 +30,20 @@ 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\Resource; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Entity\UserList; use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Table\Resource; +use VuFind\Db\Entity\UserResource; +use VuFind\Log\LoggerAwareTrait; -use function count; +use function in_array; /** * Database service for resource. @@ -44,19 +51,39 @@ * @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 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 callable $resourcePopulatorLoader Resource populator + */ + public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, + callable $resourcePopulatorLoader + ) { + $this->resourcePopulatorLoader = $resourcePopulatorLoader; + parent::__construct($entityManager, $entityPluginManager); } /** @@ -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,11 @@ public function rollBackTransaction(): void */ public function getResourceById(int $id): ?ResourceEntityInterface { - return $this->resourceTable->select(['id' => $id])->current(); + $resource = $this->entityManager->find( + $this->getEntityClass(\VuFind\Db\Entity\Resource::class), + $id + ); + return $resource; } /** @@ -111,7 +142,8 @@ public function getResourceById(int $id): ?ResourceEntityInterface */ public function createEntity(): ResourceEntityInterface { - return $this->resourceTable->createRow(); + $class = $this->getEntityClass(Resource::class); + return new $class(); } /** @@ -122,12 +154,65 @@ 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 ' . $this->getEntityClass(Resource::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; + } + + /** + * 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 + */ + public static function getOrderByClause(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'); } /** @@ -140,7 +225,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 +238,12 @@ 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($this->getEntityClass(Resource::class)); + $criteria = [ + 'recordId' => $ids, + 'source' => $source, + ]; + return $repo->findBy($criteria); } /** @@ -183,17 +269,52 @@ 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(User::class, $userOrId); + $list = $listOrId ? $this->getDoctrineReference(UserList::class, $listOrId) : null; + $orderByDetails = empty($sort) ? [] : ResourceService::getOrderByClause($sort); + $dql = 'SELECT DISTINCT r'; + if (!empty($orderByDetails['extraSelect'])) { + $dql .= ', ' . $orderByDetails['extraSelect']; + } + $dql .= ' FROM ' . $this->getEntityClass(Resource::class) . ' r ' + . 'JOIN ' . $this->getEntityClass(UserResource::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)) { + $linkingTable = $this->getDbService(TagService::class); + $matches = []; + foreach ($tags as $tag) { + $matches[] = $linkingTable + ->getResourceIDsForTag($tag, $user->getId(), $list?->getId(), $caseSensitiveTags); + } + $dqlWhere[] = 'r.id IN (:ids)'; + $parameters['ids'] = $matches; + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + //$dql .= ' GROUP BY r.id'; + 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 +329,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 ' . $this->getEntityClass(Resource::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 +347,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 ' . $this->getEntityClass(Resource::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 +363,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(UserResource::class, $resourceOrId)); } } diff --git a/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php b/module/VuFind/src/VuFind/Db/Service/ResourceServiceFactory.php index 11a13dad506..35bfd080183 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..95b1f354c3a 100644 --- a/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php +++ b/module/VuFind/src/VuFind/Db/Service/ResourceTagsService.php @@ -30,14 +30,22 @@ 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\Resource; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\ResourceTags; use VuFind\Db\Entity\ResourceTagsEntityInterface; +use VuFind\Db\Entity\Tags; use VuFind\Db\Entity\TagsEntityInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Entity\UserList; use VuFind\Db\Entity\UserListEntityInterface; -use function is_int; +use function count; +use function in_array; /** * Database service for resource_tags. @@ -49,11 +57,33 @@ * @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']; + $newOrder = []; + foreach ($order as $next) { + if (in_array($next, $legalSorts)) { + $newOrder[] = $next . 'Sort ASC'; + } + } + return $newOrder; + } /** * Begin a database transaction. @@ -63,7 +93,7 @@ class ResourceTagsService extends AbstractDbService implements */ public function beginTransaction(): void { - $this->getDbTable('ResourceTags')->beginTransaction(); + $this->entityManager->getConnection()->beginTransaction(); } /** @@ -74,7 +104,7 @@ public function beginTransaction(): void */ public function commitTransaction(): void { - $this->getDbTable('ResourceTags')->commitTransaction(); + $this->entityManager->getConnection()->commit(); } /** @@ -85,7 +115,7 @@ public function commitTransaction(): void */ public function rollBackTransaction(): void { - $this->getDbTable('ResourceTags')->rollbackTransaction(); + $this->entityManager->getConnection()->rollBack(); } /** @@ -110,8 +140,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, u.username AS username, r.title AS title,' + . ' t.id AS tag_id, r.id AS resource_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 ' . $this->getEntityClass(ResourceTags::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 +194,8 @@ public function getResourceTagsPaginator( */ public function createEntity(): ResourceTagsEntityInterface { - return $this->getDbTable('ResourceTags')->createRow(); + $class = $this->getEntityClass(ResourceTags::class); + return new $class(); } /** @@ -142,41 +216,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(Tags::class, $tagOrId); + $dql = ' SELECT rt FROM ' . $this->getEntityClass(ResourceTags::class) . ' rt '; + $dqlWhere = ['rt.tag = :tag ']; + $parameters = compact('tag'); + + if (null !== $resourceOrId) { + $resource = $this->getDoctrineReference(Resource::class, $resourceOrId); + $dqlWhere[] = 'rt.resource = :resource '; + $parameters['resource'] = $resource; + } else { + $resource = null; + $dqlWhere[] = 'rt.resource IS NULL '; + } + + if (null !== $listOrId) { + $list = $this->getDoctrineReference(UserList::class, $listOrId); + $dqlWhere[] = 'rt.list = :list '; + $parameters['list'] = $list; + } else { + $list = null; + $dqlWhere[] = 'rt.list IS NULL '; + } + + if (null !== $userOrId) { + $user = $this->getDoctrineReference(User::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 +278,50 @@ public function createLink( */ public function deleteLinksByResourceTagsIdArray(array $ids): int { - return $this->getDbTable('ResourceTags')->deleteByIdArray($ids); + $dql = 'DELETE FROM ' . $this->getEntityClass(ResourceTags::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 ' . $this->getEntityClass(ResourceTags::class) . ' rt '; + + $dqlWhere = ['rt.user = :user ']; + $parameters = ['user' => $this->getDoctrineReference(User::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 +342,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 +366,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 +386,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 +405,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(UserList::class, $listOrId); + $user = $this->getDoctrineReference(User::class, $userOrId); + $dql = 'DELETE FROM ' . $this->getEntityClass(ResourceTags::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 +433,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 ' . $this->getEntityClass(ResourceTags::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 +477,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 ' . $this->getEntityClass(ResourceTags::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 +529,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 ' . $this->getEntityClass(ResourceTags::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 +592,12 @@ public function deleteResourceTags( */ public function getAnonymousCount(): int { - return $this->getDbTable('ResourceTags')->getAnonymousCount(); + $dql = 'SELECT COUNT(rt.id) AS total ' + . 'FROM ' . $this->getEntityClass(ResourceTags::class) . ' rt ' + . 'WHERE rt.user IS NULL'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats['total']; } /** @@ -418,8 +609,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 ' . $this->getEntityClass(ResourceTags::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 +628,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 ' . $this->getEntityClass(ResourceTags::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 ' . $this->getEntityClass(ResourceTags::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 +661,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 ' . $this->getEntityClass(ResourceTags::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/SessionService.php b/module/VuFind/src/VuFind/Db/Service/SessionService.php index 1e86a5f9210..04cfa0a8aaa 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($this->getEntityClass(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($this->getEntityClass(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 = $this->getEntityClass(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,8 @@ public function garbageCollect(int $maxLifetime): int */ public function createEntity(): SessionEntityInterface { - return $this->getDbTable('Session')->createRow(); + $class = $this->getEntityClass(SessionEntityInterface::class); + return new $class(); } /** @@ -138,6 +215,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($this->getEntityClass(SessionEntityInterface::class), 's') + ->where('s.lastUsed < :used') + ->setParameter('used', $dateLimit->getTimestamp()); + if ($limit) { + $subQueryBuilder->setMaxResults($limit); + } + $queryBuilder = $this->entityManager->createQueryBuilder(); + $queryBuilder->delete($this->getEntityClass(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..080dded8f71 100644 --- a/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php +++ b/module/VuFind/src/VuFind/Db/Service/ShortlinksService.php @@ -30,10 +30,10 @@ namespace VuFind\Db\Service; +use DateTime; use Exception; +use VuFind\Db\Entity\Shortlinks; use VuFind\Db\Entity\ShortlinksEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; /** * Database service for shortlinks. @@ -46,12 +46,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 +57,7 @@ class ShortlinksService extends AbstractDbService implements */ public function beginTransaction(): void { - $this->getDbTable('shortlinks')->beginTransaction(); + $this->entityManager->getConnection()->beginTransaction(); } /** @@ -71,7 +68,7 @@ public function beginTransaction(): void */ public function commitTransaction(): void { - $this->getDbTable('shortlinks')->commitTransaction(); + $this->entityManager->getConnection()->commit(); } /** @@ -82,7 +79,7 @@ public function commitTransaction(): void */ public function rollBackTransaction(): void { - $this->getDbTable('shortlinks')->rollbackTransaction(); + $this->entityManager->getConnection()->rollBack(); } /** @@ -92,7 +89,8 @@ public function rollBackTransaction(): void */ public function createEntity(): ShortlinksEntityInterface { - return $this->getDbTable('shortlinks')->createRow(); + $class = $this->getEntityClass(Shortlinks::class); + return new $class(); } /** @@ -104,10 +102,12 @@ 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->entityManager->persist($shortlink); + $this->entityManager->flush(); + return $shortlink; } /** @@ -119,7 +119,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($this->getEntityClass(Shortlinks::class), 's') + ->where('s.hash = :hash') + ->setParameter('hash', $hash); + $query = $queryBuilder->getQuery(); + return $query->getResult()[0] ?? null; } /** @@ -129,6 +135,6 @@ public function getShortLinkByHash(string $hash): ?ShortlinksEntityInterface */ public function getShortLinksWithMissingHashes(): array { - return iterator_to_array($this->getDbTable('shortlinks')->select(['hash' => null])); + return $this->entityManager->getRepository($this->getEntityClass(Shortlinks::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 ee21731f0de..cec6eb948ea 100644 --- a/module/VuFind/src/VuFind/Db/Service/TagService.php +++ b/module/VuFind/src/VuFind/Db/Service/TagService.php @@ -29,10 +29,20 @@ namespace VuFind\Db\Service; -use Laminas\Db\Sql\Select; +use Doctrine\ORM\Query\ResultSetMapping; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\Resource; +use VuFind\Db\Entity\ResourceTags; +use VuFind\Db\Entity\Tags; use VuFind\Db\Entity\TagsEntityInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Entity\UserList; use VuFind\Db\Entity\UserListEntityInterface; +use VuFind\Db\Entity\UserResource; +use VuFind\Log\LoggerAwareTrait; + +use function count; /** * Database service for tags. @@ -43,9 +53,41 @@ * @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 LoggerAwareTrait; + + /** + * Get resources associated with a particular tag. + * + * @param string $tag Tag to match + * @param string $user ID of user owning favorite list + * @param string $list ID of list to retrieve (null for all favorites) + * @param bool $caseSensitiveTags Should tags be treated case sensitively? + * + * @return array + */ + public function getResourceIDsForTag($tag, $user, $list = null, $caseSensitiveTags = false) + { + $dql = 'SELECT DISTINCT(rt.resource) AS resource_id ' + . 'FROM ' . $this->getEntityClass(ResourceTags::class) . ' rt ' + . 'JOIN rt.tag t ' + . 'WHERE ' . ($caseSensitiveTags ? 't.tag = :tag' : 'LOWER(t.tag) = LOWER(:tag) ') + . 'AND rt.user = :user '; + + $user = $this->getDoctrineReference(User::class, $user); + $parameters = compact('tag', 'user'); + if (null !== $list) { + $list = $this->getDoctrineReference(UserList::class, $list); + $dql .= 'AND rt.list = :list'; + $parameters['list'] = $list; + } + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $result = $query->getSingleColumnResult(); + return $result; + } /** * Get statistics on use of tags. @@ -57,7 +99,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 ' . $this->getEntityClass(ResourceTags::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 +129,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 +146,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 ' . $this->getEntityClass(Tags::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 +167,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 ' . $this->getEntityClass(ResourceTags::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 +239,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) ? [] : ResourceService::getOrderByClause($sort); + $dql = 'SELECT DISTINCT(r.id) AS resource, r'; + if (!empty($orderByDetails['extraSelect'])) { + $dql .= ', ' . $orderByDetails['extraSelect']; + } + $dql .= ' FROM ' . $this->getEntityClass(Tags::class) . ' t ' + . 'JOIN ' . $this->getEntityClass(ResourceTags::class) . ' rt WITH t.id = rt.tag ' + . 'JOIN ' . $this->getEntityClass(Resource::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(User::class, $ownerOrId); + } + $dql = 'SELECT ' . $fieldList . ' FROM ' . $this->getEntityClass(Tags::class) . ' t ' + . 'JOIN ' . $this->getEntityClass(ResourceTags::class) . ' rt WITH t.id = rt.tag ' + . 'JOIN ' . $this->getEntityClass(Resource::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(User::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 +353,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 +382,21 @@ 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(UserList::class, $listOrId); + } + return $this->getRecordTagsWithDoctrine( + $id, + $source, + $limit, + $userOrId, + $sort, + $ownerOrId, + $extraClauses, + $extraParams + ); } /** @@ -226,12 +425,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(UserList::class, $listOrId); + } else { + $extraClauses[] = 'rt.list IS NOT NULL'; + } + return $this->getRecordTagsWithDoctrine( + $id, + $source, + $limit, + $userOrId, + $sort, + $ownerOrId, + $extraClauses, + $extraParams, + $caseSensitive + ); } /** @@ -257,11 +468,94 @@ 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 + ); + } + + /** + * Support method for fixDuplicateTag() -- merge $source into $target. + * + * @param TagsEntityInterface $target Target ID + * @param TagsEntityInterface $source Source ID + * + * @return void + */ + protected function mergeTags($target, $source) + { + // Don't merge a tag with itself! + if ($target->getId() === $source->getId()) { + return; + } + + $result = $this->entityManager->getRepository($this->getEntityClass(ResourceTags::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; + } + } + + /** + * Support method for fixDuplicateTags() + * + * @param string $tag Tag to deduplicate. + * @param bool $caseSensitive Treat tags as case-sensitive? + * + * @return void + */ + protected function fixDuplicateTag($tag, $caseSensitive) + { + // Make sure this really is a duplicate. + $result = $this->getTagsByText($tag, $caseSensitive); + if (count($result) < 2) { + return; + } + + $first = current($result); + foreach ($result as $current) { + $this->mergeTags($first, $current); + } + } + + /** + * Repair duplicate tags in the database (if any). + * + * @param bool $caseSensitive Treat tags as case-sensitive? + * + * @return void + */ + public function fixDuplicateTags($caseSensitive = false) + { + foreach ($this->getDuplicateTags() as $dupe) { + $this->fixDuplicateTag($dupe['tag'], $caseSensitive); + } } /** @@ -274,7 +568,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 +605,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 ' . $this->getEntityClass(ResourceTags::class) . ' rt ' + . 'JOIN rt.tag t ' + . 'JOIN rt.resource r ' + . 'JOIN ' . $this->getEntityClass(UserResource::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 +648,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(User::class, $userOrId); + $tag = $caseSensitive ? 't.tag' : 'lower(t.tag)'; + + $dql = 'SELECT MIN(t.id) AS id, ' . $tag . ' AS tag ' + . 'FROM ' . $this->getEntityClass(ResourceTags::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 +675,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 ' . $this->getEntityClass(Tags::class) . ' t ' + . 'WHERE t NOT IN (SELECT IDENTITY(rt.tag) FROM ' + . $this->getEntityClass(ResourceTags::class) . ' rt)'; + $query = $this->entityManager->createQuery($dql); + $query->execute(); } /** @@ -351,7 +691,7 @@ public function deleteOrphanedTags(): void */ public function getTagById(int $id): ?TagsEntityInterface { - return $this->getDbTable('Tags')->select(['id' => $id])->current(); + return $this->entityManager->find($this->getEntityClass(Tags::class), $id); } /** @@ -361,6 +701,7 @@ public function getTagById(int $id): ?TagsEntityInterface */ public function createEntity(): TagsEntityInterface { - return $this->getDbTable('Tags')->createRow(); + $class = $this->getEntityClass(Tags::class); + return new $class(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/UserCardService.php b/module/VuFind/src/VuFind/Db/Service/UserCardService.php index e6ba89265a9..8f6515c7d12 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserCardService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserCardService.php @@ -31,12 +31,16 @@ 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\User; +use VuFind\Db\Entity\UserCard; use VuFind\Db\Entity\UserCardEntityInterface; use VuFind\Db\Entity\UserEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Log\LoggerAwareTrait; use function count; use function is_int; @@ -53,22 +57,27 @@ */ 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 ILSAuthenticator $ilsAuthenticator ILS authenticator + * @param AccountCapabilities $capabilities Account capabilities configuration */ public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, protected ILSAuthenticator $ilsAuthenticator, protected AccountCapabilities $capabilities ) { + parent::__construct($entityManager, $entityPluginManager); } /** @@ -78,7 +87,10 @@ public function __construct( */ public function getInsecureRows(): array { - return iterator_to_array($this->getDbTable('UserCard')->getInsecureRows()); + $dql = 'SELECT UC FROM ' . $this->getEntityClass(UserCard::class) + . ' UC WHERE UC.catPassword IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + return $query->getResult(); } /** @@ -88,10 +100,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 ' . $this->getEntityClass(UserCard::class) + . ' UC WHERE UC.catUsername IS NOT NULL'; + $query = $this->entityManager->createQuery($dql); + return $query->getResult(); } /** @@ -111,17 +123,23 @@ public function getLibraryCards( if (!$this->capabilities->libraryCardsEnabled()) { return []; } - $userCard = $this->getDbTable('UserCard'); - $criteria = [ - 'user_id' => is_int($userOrId) ? $userOrId : $userOrId->getId(), - ]; - if ($id) { - $criteria['id'] = $id; + $dql = 'SELECT UC ' + . 'FROM ' . $this->getEntityClass(UserCard::class) . ' UC '; + $dqlWhere = ['UC.user = :user']; + $parameters['user'] = $this->getDoctrineReference(User::class, $userOrId); + if (null !== $id) { + $dqlWhere[] = 'UC.id = :id'; + $parameters['id'] = $id; } - if ($catUsername) { - $criteria['cat_username'] = $catUsername; + if (null !== $catUsername) { + $dqlWhere[] = 'UC.catUsername = :catUsername'; + $parameters['catUsername'] = $catUsername; } - return iterator_to_array($userCard->select($criteria)); + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $records = $query->getResult(); + return $records; } /** @@ -138,13 +156,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(User::class, $userOrId)) ->setCatUsername('') ->setRawCatPassword(''); } else { @@ -175,10 +190,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 @@ -241,7 +259,7 @@ public function persistLibraryCardData( $row = ($id !== null) ? current($this->getLibraryCards($user, $id)) : null; if (empty($row)) { $row = $this->createEntity() - ->setUser($user) + ->setUser($this->getDoctrineReference(User::class, $user)) ->setCreated(new DateTime()); } $row->setCardName($cardName); @@ -294,7 +312,7 @@ public function synchronizeUserLibraryCardData(UserEntityInterface|int $userOrId $row = current($this->getLibraryCards($user, catUsername: $user->getCatUsername())); if (empty($row)) { $row = $this->createEntity() - ->setUser($user) + ->setUser($this->getDoctrineReference(User::class, $user)) ->setCatUsername($user->getCatUsername()) ->setCardName($user->getCatUsername()) ->setCreated(new DateTime()); @@ -340,6 +358,7 @@ public function activateLibraryCard(UserEntityInterface|int $userOrId, int $id): */ public function createEntity(): UserCardEntityInterface { - return $this->getDbTable('UserCard')->createRow(); + $class = $this->getEntityClass(UserCard::class); + return new $class(); } } diff --git a/module/VuFind/src/VuFind/Db/Service/UserListService.php b/module/VuFind/src/VuFind/Db/Service/UserListService.php index 2767fb2e7a7..4a90ba86036 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserListService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserListService.php @@ -31,16 +31,18 @@ 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\Resource; +use VuFind\Db\Entity\ResourceTags; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Entity\UserList; use VuFind\Db\Entity\UserListEntityInterface; -use VuFind\Db\Table\DbTableAwareInterface; -use VuFind\Db\Table\DbTableAwareTrait; +use VuFind\Db\Entity\UserResource; use VuFind\Exception\RecordMissing as RecordMissingException; +use VuFind\Log\LoggerAwareTrait; -use function is_int; +use function count; /** * Database service for UserList. @@ -52,9 +54,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 +69,8 @@ class UserListService extends AbstractDbService implements DbTableAwareInterface */ public function createEntity(): UserListEntityInterface { - return $this->getDbTable('UserList')->createRow(); + $class = $this->getEntityClass(UserList::class); + return new $class(); } /** @@ -75,8 +82,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(UserList::class, $listOrId)); } /** @@ -89,7 +95,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 +112,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 ' . $this->getEntityClass(UserList::class) . ' ul '; + + $parameters = []; + $where = ["ul.public = '1'"]; + if (!empty($includeFilter)) { + $where[] = 'ul.id 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 +143,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 ' . $this->getEntityClass(UserList::class) . ' ul ' + . 'LEFT JOIN ' . $this->getEntityClass(UserResource::class) . ' ur WITH ur.list = ul.id ' + . 'WHERE ul.user = :user ' + . 'GROUP BY ul ' + . 'ORDER BY ul.title'; + + $parameters = ['user' => $this->getDoctrineReference(User::class, $userOrId)]; + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $results = $query->getResult(); + return $results; } /** @@ -188,13 +176,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 ' . $this->getEntityClass(ResourceTags::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 +229,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 ' . $this->getEntityClass(UserList::class) . ' ul ' + . 'WHERE ul.user = :user ' + . 'ORDER BY ul.title'; + + $parameters = ['user' => $this->getDoctrineReference(User::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 ' . $this->getEntityClass(UserList::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 +274,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 ' . $this->getEntityClass(UserList::class) . ' ul ' + . 'JOIN ' . $this->getEntityClass(UserResource::class) . ' ur WITH ur.list = ul.id ' + . 'JOIN ' . $this->getEntityClass(Resource::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..8fc01be212c 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserResourceService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserResourceService.php @@ -29,15 +29,16 @@ namespace VuFind\Db\Service; -use Exception; +use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\Resource; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; +use VuFind\Db\Entity\UserList; 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 +50,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 ' . $this->getEntityClass(UserResource::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 +93,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 ' . $this->getEntityClass(UserResource::class) . ' ur ' + . 'JOIN ' . $this->getEntityClass(Resource::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(User::class, $userOrId); + } + if (null !== $listOrId) { + $dql .= 'AND ur.list = :list'; + $parameters['list'] = $this->getDoctrineReference(UserList::class, $listOrId); + } + + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + return $query->getResult(); } /** @@ -88,7 +119,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 ' . $this->getEntityClass(UserResource::class) . ' u'; + $query = $this->entityManager->createQuery($dql); + $stats = current($query->getResult()); + return $stats; } /** @@ -107,27 +145,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(Resource::class, $resourceOrId); + $user = $this->getDoctrineReference(User::class, $userOrId); + $list = $this->getDoctrineReference(UserList::class, $listOrId); + $params = compact('resource', 'list', 'user'); + $result = current($this->entityManager->getRepository($this->getEntityClass(UserResource::class)) + ->findBy($params)); + + if (empty($result)) { $result = $this->createEntity() ->setResource($resource) ->setUser($user) @@ -135,7 +160,12 @@ public function createOrUpdateLink( } // Update the notes: $result->setNotes($notes); - $this->persistEntity($result); + try { + $this->persistEntity($result); + } catch (\Exception $e) { + $this->logError('Could not save user resource: ' . $e->getMessage()); + return false; + } return $result; } @@ -155,21 +185,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(User::class, $userOrId); + $dql = 'DELETE FROM ' . $this->getEntityClass(UserResource::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(UserList::class, $listOrId); + } + $dql .= ' WHERE ' . implode(' AND ', $dqlWhere); + $query = $this->entityManager->createQuery($dql); + $query->setParameters($parameters); + $query->execute(); } /** @@ -179,7 +210,8 @@ public function unlinkFavorites( */ public function createEntity(): UserResourceEntityInterface { - return $this->getDbTable('UserResource')->createRow(); + $class = $this->getEntityClass(UserResource::class); + return new $class(); } /** @@ -192,7 +224,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 ' . $this->getEntityClass(UserResource::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 +239,47 @@ public function changeResourceId(int $old, int $new): void */ public function deduplicate(): void { - $this->getDbTable('UserResource')->deduplicate(); + $repo = $this->entityManager->getRepository($this->getEntityClass(UserResource::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(UserResource::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 ' . $this->getEntityClass(UserResource::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..ab00a15b4f9 100644 --- a/module/VuFind/src/VuFind/Db/Service/UserService.php +++ b/module/VuFind/src/VuFind/Db/Service/UserService.php @@ -29,9 +29,12 @@ namespace VuFind\Db\Service; +use Doctrine\ORM\EntityManager; use Laminas\Log\LoggerAwareInterface; 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; @@ -50,19 +53,27 @@ class UserService extends AbstractDbService implements DbTableAwareInterface, LoggerAwareInterface, + DbServiceAwareInterface, UserServiceInterface, UserSessionPersistenceInterface { use DbTableAwareTrait; use LoggerAwareTrait; + use DbServiceAwareTrait; /** * Constructor * - * @param SessionContainer $userSessionContainer Session container for user data + * @param EntityManager $entityManager Doctrine ORM entity manager + * @param EntityPluginManager $entityPluginManager VuFind entity plugin manager + * @param SessionContainer $userSessionContainer Session container for user data */ - public function __construct(protected SessionContainer $userSessionContainer) - { + public function __construct( + EntityManager $entityManager, + EntityPluginManager $entityPluginManager, + protected SessionContainer $userSessionContainer + ) { + parent::__construct($entityManager, $entityPluginManager); } /** @@ -93,13 +104,13 @@ public function deleteUser(UserEntityInterface|int $userOrId): void /** * 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); + return $this->entityManager->find($this->getEntityClass(User::class), $id); } /** @@ -113,17 +124,22 @@ 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); + // 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', + ]; + if (isset($legalFieldMap[$fieldName])) { + $dql = 'SELECT U FROM ' . $this->getEntityClass(User::class) . ' U ' + . 'WHERE U.' . $legalFieldMap[$fieldName] . ' = :fieldValue'; + $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'); } 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 dabce7f53d2..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 baf35c287f6..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/CaseSensitiveTagsFactory.php b/module/VuFind/src/VuFind/Db/Table/CaseSensitiveTagsFactory.php deleted file mode 100644 index f37b78766ce..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/CaseSensitiveTagsFactory.php +++ /dev/null @@ -1,76 +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; - -/** - * Shared Tags / ResourceTags 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 CaseSensitiveTagsFactory 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!'); - } - $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]); - } -} 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 217c45cb348..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 02bd7c92507..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Comments.php +++ /dev/null @@ -1,177 +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(); - } -} 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 a9560d8e1a4..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/Feedback.php +++ /dev/null @@ -1,153 +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()->columns( - [ - '*', - 'user_name' => new \Laminas\Db\Sql\Expression( - "CONCAT_WS(' ', u.firstname, u.lastname)" - ), - 'manager_name' => new \Laminas\Db\Sql\Expression( - "CONCAT_WS(' ', m.firstname, m.lastname)" - ), - ] - ); - 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->join( - ['u' => 'user'], - 'u.id = feedback.user_id', - [], - $select::JOIN_LEFT - )->join( - ['m' => 'user'], - 'm.id = feedback.updated_by', - [], - $select::JOIN_LEFT - )->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/LoginToken.php b/module/VuFind/src/VuFind/Db/Table/LoginToken.php deleted file mode 100644 index 6b7cea69310..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 8d6eb4e4be0..00000000000 --- a/module/VuFind/src/VuFind/Db/Table/OaiResumption.php +++ /dev/null @@ -1,110 +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; -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 - */ - public function findToken($token) - { - return $this->select(['id' => $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 index 73412ae0d1a..adf76aba9ed 100644 --- a/module/VuFind/src/VuFind/Db/Table/PluginManager.php +++ b/module/VuFind/src/VuFind/Db/Table/PluginManager.php @@ -46,25 +46,11 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @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, ]; @@ -74,25 +60,11 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @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, ]; diff --git a/module/VuFind/src/VuFind/Db/Table/Ratings.php b/module/VuFind/src/VuFind/Db/Table/Ratings.php index 9de74229a3b..0397ce62851 100644 --- a/module/VuFind/src/VuFind/Db/Table/Ratings.php +++ b/module/VuFind/src/VuFind/Db/Table/Ratings.php @@ -30,12 +30,7 @@ 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 @@ -46,10 +41,8 @@ * @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 +class Ratings extends Gateway { - use DbServiceAwareTrait; - /** * Constructor * @@ -68,170 +61,4 @@ public function __construct( ) { 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(); - } } 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 9bce10bb994..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 b97f72114a7..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 05ca5fea7db..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' => '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->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/Session.php b/module/VuFind/src/VuFind/Db/Table/Session.php index 0c6a01aee7b..957ba8dc01e 100644 --- a/module/VuFind/src/VuFind/Db/Table/Session.php +++ b/module/VuFind/src/VuFind/Db/Table/Session.php @@ -33,9 +33,6 @@ use Laminas\Db\Adapter\Adapter; use VuFind\Db\Row\RowGateway; -use VuFind\Exception\SessionExpired as SessionExpiredException; - -use function intval; /** * Table Definition for session @@ -70,98 +67,6 @@ public function __construct( 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. * 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/UserCard.php b/module/VuFind/src/VuFind/Db/Table/UserCard.php deleted file mode 100644 index a23e3324f6e..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 2c48078a49f..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 8b2cdb29a3b..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 index b262fa290c1..cf8456d62d5 100644 --- a/module/VuFind/src/VuFind/Db/Table/UserResource.php +++ b/module/VuFind/src/VuFind/Db/Table/UserResource.php @@ -35,7 +35,6 @@ 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; /** @@ -142,173 +141,4 @@ public function createOrUpdateLink( 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/VuFind/Favorites/FavoritesService.php b/module/VuFind/src/VuFind/Favorites/FavoritesService.php index 3f4ed44a272..1827885150f 100644 --- a/module/VuFind/src/VuFind/Favorites/FavoritesService.php +++ b/module/VuFind/src/VuFind/Favorites/FavoritesService.php @@ -33,6 +33,7 @@ use Laminas\Session\Container; use Laminas\Stdlib\Parameters; use VuFind\Db\Entity\ResourceEntityInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Entity\UserListEntityInterface; use VuFind\Db\Service\Feature\TransactionInterface; @@ -113,7 +114,8 @@ public function createListForUser(?UserEntityInterface $user): UserListEntityInt return $this->userListService->createEntity() ->setCreated(new DateTime()) - ->setUser($user); + // Stopgap until we've fully converted to Doctrine: + ->setUser($this->userListService->getDoctrineReference(User::class, $user)); } /** diff --git a/module/VuFind/src/VuFind/Form/Handler/Database.php b/module/VuFind/src/VuFind/Form/Handler/Database.php index a5b019cf670..cd49e97029f 100644 --- a/module/VuFind/src/VuFind/Form/Handler/Database.php +++ b/module/VuFind/src/VuFind/Form/Handler/Database.php @@ -33,8 +33,10 @@ namespace VuFind\Form\Handler; use Laminas\Log\LoggerAwareInterface; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; use VuFind\Db\Service\FeedbackServiceInterface; +use VuFind\Db\Service\UserService; use VuFind\Log\LoggerAwareTrait; /** @@ -54,10 +56,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 ) { } @@ -82,7 +86,7 @@ public function handle( unset($formData['message']); $now = new \DateTime(); $data = $this->feedbackService->createEntity() - ->setUser($user) + ->setUser($user ? $this->userService->getDoctrineReference(User::class, $user) : null) ->setMessage($fields['message'] ?? '') ->setFormData($formData) ->setFormName($form->getFormId()) diff --git a/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php b/module/VuFind/src/VuFind/Form/Handler/DatabaseFactory.php index 369d9080469..53b831d956f 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/RecordDriver/AbstractBase.php b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php index 74a1317e105..8befe54f0a5 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; @@ -178,80 +177,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 - ); - } - - /** - * Add tags to the record. - * - * @param UserEntityInterface $user The user posting the tag - * @param array $tags The user-provided tags - * - * @return void - * - * @deprecated Use \VuFind\Tags\TagsService::linkTagsToRecord() - */ - public function addTags($user, $tags) - { - $resources = $this->getDbTable('Resource'); - $resource = $resources->findResource( - $this->getUniqueId(), - $this->getSourceIdentifier() - ); - foreach ($tags as $tag) { - $resource->addTag($tag, $user); - } - } - - /** - * Remove tags from the record. - * - * @param UserEntityInterface $user The user posting the tag - * @param array $tags The user-provided tags - * - * @return void - * - * @deprecated Use \VuFind\Tags\TagsService::unlinkTagsFromRecord() - */ - public function deleteTags($user, $tags) - { - $resources = $this->getDbTable('Resource'); - $resource = $resources->findResource( - $this->getUniqueId(), - $this->getSourceIdentifier() - ); - foreach ($tags as $tag) { - $resource->deleteTag($tag, $user); - } - } - /** * Get rating information for this record. * @@ -308,30 +233,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. * 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/Tags/TagsService.php b/module/VuFind/src/VuFind/Tags/TagsService.php index 7d7fe9d68d6..92bc446611e 100644 --- a/module/VuFind/src/VuFind/Tags/TagsService.php +++ b/module/VuFind/src/VuFind/Tags/TagsService.php @@ -38,8 +38,6 @@ 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; @@ -54,10 +52,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 * @@ -218,7 +214,7 @@ public function unlinkTagsFromRecord(RecordDriver $driver, UserEntityInterface $ */ public function fixDuplicateTags(): void { - $this->getDbTable('Tags')->fixDuplicateTags($this->caseSensitive); + $this->tagDbService->fixDuplicateTags(); } /** diff --git a/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php b/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php index 12797276358..fc301d7b879 100644 --- a/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php +++ b/module/VuFind/src/VuFindTest/Feature/LiveDatabaseTrait.php @@ -45,8 +45,6 @@ use VuFind\Record\ResourcePopulator; use VuFindTest\Container\MockContainer; -use function count; - /** * Mix-in for accessing a real database during testing. * @@ -69,29 +67,142 @@ 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') + ); + $entityManagerFactory = new \DoctrineORMModule\Service\EntityManagerFactory( + 'orm_vufind' + ); + $container->set( + \Doctrine\ORM\EntityManager::class, + $entityManagerFactory($container, 'orm_vufind') + ); + $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, []) + ); + } + + /** + * 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); + $configManager = new \VuFind\Config\PluginManager( + $container, + $config['vufind']['config_reader'] + ); + $container->set(\VuFind\Config\PluginManager::class, $configManager); + $this->addPathResolverToContainer($container); + $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); - $configManager = new \VuFind\Config\PluginManager( - $container, - $config['vufind']['config_reader'] - ); - $container->set(\VuFind\Config\PluginManager::class, $configManager); - $this->addPathResolverToContainer($container); + $container = $this->getMockContainerWithDoctrineDependencies(); + $configManager = $container->get(\VuFind\Config\PluginManager::class); $adapterFactory = new \VuFind\Db\AdapterFactory( $configManager->get('config') ); @@ -99,7 +210,6 @@ public function getLiveDatabaseContainer(): MockContainer \Laminas\Db\Adapter\Adapter::class, $adapterFactory->getAdapter() ); - $container->set('config', $config); $container->set(\VuFind\Log\Logger::class, $this->createMock(\Laminas\Log\LoggerInterface::class)); $container->set( \VuFind\Db\Row\PluginManager::class, @@ -206,17 +316,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!" ); 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 58% 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..fb0f4d74170 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); // 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') + ); // 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') + ); + $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') ); - $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') + ); // Clean up after ourselves: - $tracker->delete(['core' => $core]); + $tracker->deleteRows($core); } } diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php index c6dd3aed41f..b785ff351a6 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php @@ -616,7 +616,9 @@ protected function getMockPluginManager(): PluginManager */ protected function getMockUser(): MockObject&UserEntityInterface { - $user = $this->createMock(UserEntityInterface::class); + // Temporary workaround until Laminas --> Doctrine migration is complete: + $user = $this->createMock(\VuFind\Db\Row\User::class); + //$user = $this->createMock(UserEntityInterface::class); $user->method('getId')->willReturn(-1); return $user; } 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..d96c461aca1 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/FeedbackServiceTest.php @@ -0,0 +1,160 @@ + + * @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\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 + { + $feedbackService = $this->getConfiguredFeedbackService()['feedbackService']; + + $this->assertInstanceOf(Feedback::class, $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\Feedback 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\Feedback 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, CONCAT(u.firstname, ' ', u.lastname) AS user_name, " + . "CONCAT(m.firstname, ' ', m.lastname) AS manager_name FROM " + . "VuFind\Db\Entity\Feedback f LEFT JOIN f.user u LEFT JOIN f.updatedBy m " + . '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); + $entityPluginManager->expects($this->once())->method('get') + ->with($this->equalTo(Feedback::class)) + ->willReturn(new Feedback()); + $feedbackService = new FeedbackService($entityManager, $entityPluginManager); + 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 new file mode 100644 index 00000000000..fe1662bce6d --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/OaiResumptionServiceTest.php @@ -0,0 +1,231 @@ + + * @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 VuFind\Db\Entity\OaiResumption; + +/** + * OaiResumptionService 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 OaiResumptionServiceTest extends \PHPUnit\Framework\TestCase +{ + use \VuFindTest\Feature\ReflectionTrait; + + /** + * OaiResumption service object to test. + * + * @param MockObject $entityManager Mock entity manager object + * @param MockObject $pluginManager Mock plugin manager object + * @param ?MockObject $oaiResumption Mock OaiResumption entity object + * + * @return MockObject + */ + protected function getService( + $entityManager, + $pluginManager, + $oaiResumption = null, + ) { + $serviceMock = $this->getMockBuilder( + \VuFind\Db\Service\OaiResumptionService::class + ) + ->onlyMethods(['createEntity']) + ->setConstructorArgs([$entityManager, $pluginManager]) + ->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 + */ + protected function getPluginManager($setExpectation = false) + { + $pluginManager = $this->getMockBuilder( + \VuFind\Db\Entity\PluginManager::class + )->disableOriginalConstructor() + ->getMock(); + if ($setExpectation) { + $pluginManager->expects($this->once())->method('get') + ->with($this->equalTo(OaiResumption::class)) + ->willReturn(new OaiResumption()); + } + return $pluginManager; + } + + /** + * Mock entity manager. + * + * @param int $count Expectation count + * + * @return MockObject + */ + protected function getEntityManager($count = 0) + { + $entityManager = $this->getMockBuilder(\Doctrine\ORM\EntityManager::class) + ->disableOriginalConstructor() + ->getMock(); + $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(true); + $resumptionService = $this->getService($entityManager, $pluginManager); + $queryStmt = "DELETE FROM VuFind\Db\Entity\OaiResumption O WHERE O.expires <= :now"; + + $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($this->anything()) + ->willReturn($query); + $resumptionService->removeExpired(); + } + + /** + * Test retrieving a row from the database based on primary key. + * + * @return void + */ + public function testfindToken(): void + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $resumptionService = $this->getService($entityManager, $pluginManager); + $queryStmt = "SELECT O FROM VuFind\Db\Entity\OaiResumption O WHERE O.id = :token"; + + $query = $this->getMockBuilder(\Doctrine\ORM\AbstractQuery::class) + ->disableOriginalConstructor() + ->onlyMethods(['getResult', 'setParameters']) + ->getMockForAbstractClass(); + $entityManager->expects($this->once())->method('createQuery') + ->with($this->equalTo($queryStmt)) + ->willReturn($query); + $oaiResumption = $this->getMockBuilder(\VuFind\Db\Entity\OaiResumption::class) + ->disableOriginalConstructor() + ->getMock(); + $query->expects($this->once())->method('getResult') + ->willReturn([$oaiResumption]); + $query->expects($this->once())->method('setParameters') + ->with(['token' => 'foo']) + ->willReturn($query); + $this->assertEquals($oaiResumption, $resumptionService->findToken('foo')); + } + + /** + * 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 testEncodeParams(array $params): void + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(); + $resumptionService = $this->getService($entityManager, $pluginManager); + $this->assertEquals( + 'cursor=20&cursorMark=100&foo=bar', + $this->callMethod($resumptionService, 'encodeParams', [$params]) + ); + } + + /** + * Test creating and persisting a new token. + * + * @return void + */ + public function testCreateAndPersistToken(): void + { + $entityManager = $this->getEntityManager(1); + $pluginManager = $this->getPluginManager(); + $oaiResumption = $this->getMockBuilder(\VuFind\Db\Entity\OaiResumption::class) + ->disableOriginalConstructor() + ->getMock(); + $params = ['cursor' => 20, + 'cursorMark' => 100, + 'foo' => 'bar']; + $queryString = 'cursor=20&cursorMark=100&foo=bar'; + $oaiResumption->expects($this->once())->method('setResumptionParameters') + ->with($queryString) + ->willReturn($oaiResumption); + $oaiResumption->expects($this->once())->method('setExpiry') + ->with($this->anything()) + ->willReturn($oaiResumption); + $oaiResumption->expects($this->once())->method('getId') + ->willReturn(1); + $resumptionService = $this->getService($entityManager, $pluginManager, $oaiResumption); + $this->assertEquals(1, $resumptionService->createAndPersistToken($params, 1666782990)->getId()); + } +} 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..71b119806c1 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Service/SessionServiceTest.php @@ -0,0 +1,350 @@ + + * @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 VuFind\Db\Entity\Session; +use VuFind\Db\Entity\SessionEntityInterface; +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 MockObject + */ + protected function getPluginManager($setExpectation = false) + { + $pluginManager = $this->getMockBuilder( + \VuFind\Db\Entity\PluginManager::class + )->disableOriginalConstructor() + ->getMock(); + if ($setExpectation) { + $pluginManager->expects($this->once())->method('get') + ->with(SessionEntityInterface::class) + ->willReturn(new Session()); + } + return $pluginManager; + } + + /** + * Mock entity manager. + * + * @param int $count Expectation count + * + * @return MockObject + */ + protected function getEntityManager($count = 0) + { + $entityManager = $this->getMockBuilder(\Doctrine\ORM\EntityManager::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager->expects($this->exactly($count))->method('persist'); + $entityManager->expects($this->exactly($count))->method('flush'); + return $entityManager; + } + + /** + * Mock queryBuilder + * + * @param string $parameter Input query parameter + * @param array $result Expected return value of getResult method. + * + * @return MockObject + */ + protected function getQueryBuilder($parameter, $result) + { + $queryBuilder = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $queryBuilder->expects($this->once())->method('select') + ->with('s') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once())->method('from') + ->with(Session::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 MockObject $entityManager Mock entity manager object + * @param MockObject $pluginManager Mock plugin manager object + * @param ?MockObject $session Mock session entity object + * + * @return MockObject + */ + protected function getService( + $entityManager, + $pluginManager, + $session = null, + ) { + $serviceMock = $this->getMockBuilder(SessionService::class) + ->onlyMethods(['createEntity']) + ->setConstructorArgs([$entityManager, $pluginManager]) + ->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() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $queryBuilder = $this->getQueryBuilder('1', [$session]); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager); + $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() + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $queryBuilder = $this->getQueryBuilder('1', []); + $entityManager->expects($this->once())->method('createQueryBuilder') + ->willReturn($queryBuilder); + $service = $this->getService($entityManager, $pluginManager); + $this->assertNull($service->getSessionById('1', false)); + } + + /** + * Test creating a new session if no existing session is found. + * + * @return void + */ + public function testCreatingSession() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager = $this->getEntityManager(1); + $pluginManager = $this->getPluginManager(true); + $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, $session); + $this->assertEquals($session, $service->getSessionById('1', true)); + } + + /** + * Test reading session data. + * + * @return void + */ + public function testReadSession() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager = $this->getEntityManager(1); + $pluginManager = $this->getPluginManager(true); + $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); + $this->assertEquals('foo', $service->readSession('1', 10000000)); + } + + /** + * Test reading expired session data. + * + * @return void + */ + public function testReadingExpiredSession() + { + $this->expectException(\VuFind\Exception\SessionExpired::class); + $this->expectExceptionMessage('Session expired!'); + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $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); + $service->readSession('1', 100); + } + + /** + * Test storing session data. + * + * @return void + */ + public function testWriteSession() + { + $session = $this->getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->getMock(); + $entityManager = $this->getEntityManager(1); + $pluginManager = $this->getPluginManager(true); + $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); + $this->assertEquals(true, $service->WriteSession('1', 'foo')); + } + + /** + * Test destroying the session. + * + * @return void + */ + public function testDestroySession() + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $queryBuilder = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + $queryBuilder->expects($this->once())->method('delete') + ->with(Session::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); + $service->destroySession('1'); + } + + /** + * Test destroying the expired sessions. + * + * @return void + */ + public function testGarbageCollect() + { + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); + $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\Session 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(Session::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); + $this->assertEquals(5, $service->garbageCollect(10000)); + } +} 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..04a0810df9c 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Favorites/FavoritesServiceTest.php @@ -30,10 +30,12 @@ namespace VuFindTest\Favorites; use PHPUnit\Framework\MockObject\MockObject; +use VuFind\Db\Entity\User; use VuFind\Db\Entity\UserEntityInterface; 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,8 +113,10 @@ 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); + $listService->expects($this->once())->method('getDoctrineReference')->with(User::class, $user) + ->willReturn($user); $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..cfb5a2ebe3e 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 @@ -32,8 +32,10 @@ use Laminas\Mvc\Controller\Plugin\Params; use PHPUnit\Framework\MockObject\MockObject; use VuFind\Db\Entity\FeedbackEntityInterface; +use VuFind\Db\Entity\User; 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 +82,10 @@ 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); + $userService->expects($this->once())->method('getDoctrineReference')->with(User::class, $user) + ->willReturn($user); + $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 +106,9 @@ 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); + $userService->expects($this->never())->method('getDoctrineReference'); + $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..efe725bea9b 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,20 @@ namespace VuFindTest\OAuth2\Repository; use PHPUnit\Framework\MockObject\MockObject; -use VuFind\Db\Row\AccessToken as AccessTokenRow; +use VuFind\Db\Entity\AccessToken; +use VuFind\Db\Entity\AccessTokenEntityInterface; use VuFind\Db\Row\User as UserRow; 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 +57,8 @@ abstract class AbstractTokenRepositoryTestCase extends \PHPUnit\Framework\TestCa { protected $accessTokenTable = []; + public $entityManager = null; + /** * Create AccessTokenRepository with mocks. * @@ -107,45 +111,6 @@ protected function getOAuth2Config(): array return ['Server' => ['userIdentifierField' => 'id']]; } - /** - * Create AccessToken table - * - * @return MockObject&AccessToken - */ - protected function getMockAccessTokenTable(): AccessToken - { - $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) - ->disableOriginalConstructor() - ->onlyMethods(['getByIdAndType']) - ->getMock(); - $accessTokenTable->expects($this->any()) - ->method('getByIdAndType') - ->willReturnCallback($getByIdAndTypeCallback); - - return $accessTokenTable; - } - /** * Create User table * @@ -172,57 +137,123 @@ protected function getMockUserTable(): User } /** - * Create AccessToken row + * Create User row * * @param array $data Row data * - * @return MockObject&AccessTokenRow + * @return MockObject&UserRow */ - protected function createAccessTokenRow(array $data): AccessTokenRow + protected function createUserRow(array $data): UserRow { - $result = $this->getMockBuilder(AccessTokenRow::class) + $result = $this->getMockBuilder(UserRow::class) ->disableOriginalConstructor() - ->onlyMethods(['initialize', 'save']) + ->onlyMethods(['initialize']) ->getMock(); $result->populate($data); + return $result; + } - $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; - } - } - $this->accessTokenTable[] = $data; - return 1; - }; + /** + * Mock entity manager. + * + * @return MockObject + */ + protected function getEntityManager() + { + $entityManager = $this->getMockBuilder(\Doctrine\ORM\EntityManager::class) + ->disableOriginalConstructor() + ->onlyMethods(['createQuery','persist','flush']) + ->getMock(); + $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; + } - $result->expects($this->any()) - ->method('save') - ->willReturnCallback($save); + /** + * Mock entity plugin manager. + * + * @param bool $setExpectation Flag to set the method expectations. + * + * @return MockObject + */ + protected function getPluginManager($setExpectation = false) + { + $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; + } - return $result; + /** + * Create a mock AccessTokenEntity from an array of values. + * + * @param array $fields Field values + * + * @return AccessTokenEntityInterface&MockObject + */ + protected function createAccessTokenEntity(array $fields): AccessTokenEntityInterface&MockObject + { + $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) { + $userTable = $this->getMockUserTable(); + return $userTable->getById($userId); + } + 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; } /** @@ -232,26 +263,95 @@ protected function createUserRow(array $data): UserRow */ protected function getMockAccessTokenService(): AccessTokenServiceInterface { - $accessTokenTable = $this->getMockAccessTokenTable(); + $entityManager = $this->getEntityManager(); + $pluginManager = $this->getPluginManager(true); $accessTokenService = $this->getMockBuilder(AccessTokenService::class) ->disableOriginalConstructor() ->onlyMethods( [ + 'createEntity', 'getByIdAndType', 'getNonce', 'storeNonce', ] ) + ->setConstructorArgs([$entityManager, $pluginManager]) ->getMock(); + $accessTokenService = $this->getMockBuilder(AccessTokenService::class) + ->disableOriginalConstructor() + ->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; } 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