Initialize Nest databases library.

This commit is contained in:
Madeorsk 2024-11-08 16:33:44 +01:00
commit 84424c3217
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
63 changed files with 5486 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# IDEA
.idea/
*.iml
# Composer
vendor/

57
composer.json Normal file
View file

@ -0,0 +1,57 @@
{
"version": "1.0",
"name": "nest/database",
"description": "Nest databases service.",
"type": "library",
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"autoload": {
"psr-4": {
"Nest\\Database\\": "src/"
}
},
"repositories": [
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Core"
},
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Events"
},
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Types"
},
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Cli"
},
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Model"
},
{
"type": "vcs",
"url": "https://code.zeptotech.net/Nest/Configuration"
}
],
"require": {
"php": "^8.3",
"ext-pdo": "*",
"nest/core": "dev-main",
"nest/events": "dev-main",
"nest/types": "dev-main",
"nest/cli": "1.x-dev",
"nest/model": "dev-main",
"nesbot/carbon": "^3.0",
"symfony/uid": "^7.1"
},
"suggest": {
"nest/configuration": "dev-main"
}
}

928
composer.lock generated Normal file
View file

@ -0,0 +1,928 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "23ba0fcb363fbba990800f53ca21803c",
"packages": [
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"conflict": {
"doctrine/dbal": "<4.0.0 || >=5.0.0"
},
"require-dev": {
"doctrine/dbal": "^4.0.0",
"nesbot/carbon": "^2.71.0 || ^3.0.0",
"phpunit/phpunit": "^10.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "KyleKatarn",
"email": "kylekatarnls@gmail.com"
}
],
"description": "Types to use Carbon in Doctrine",
"keywords": [
"carbon",
"date",
"datetime",
"doctrine",
"time"
],
"support": {
"issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
"source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
},
"funding": [
{
"url": "https://github.com/kylekatarnls",
"type": "github"
},
{
"url": "https://opencollective.com/Carbon",
"type": "open_collective"
},
{
"url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
"type": "tidelift"
}
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "nesbot/carbon",
"version": "3.8.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
"reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947",
"shasum": ""
},
"require": {
"carbonphp/carbon-doctrine-types": "<100.0",
"ext-json": "*",
"php": "^8.1",
"psr/clock": "^1.0",
"symfony/clock": "^6.3 || ^7.0",
"symfony/polyfill-mbstring": "^1.0",
"symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"require-dev": {
"doctrine/dbal": "^3.6.3 || ^4.0",
"doctrine/orm": "^2.15.2 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.57.2",
"kylekatarnls/multi-tester": "^2.5.3",
"ondrejmirtes/better-reflection": "^6.25.0.4",
"phpmd/phpmd": "^2.15.0",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.11.2",
"phpunit/phpunit": "^10.5.20",
"squizlabs/php_codesniffer": "^3.9.0"
},
"bin": [
"bin/carbon"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev",
"dev-2.x": "2.x-dev"
},
"laravel": {
"providers": [
"Carbon\\Laravel\\ServiceProvider"
]
},
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"Carbon\\": "src/Carbon/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "https://markido.com"
},
{
"name": "kylekatarnls",
"homepage": "https://github.com/kylekatarnls"
}
],
"description": "An API extension for DateTime that supports 281 different languages.",
"homepage": "https://carbon.nesbot.com",
"keywords": [
"date",
"datetime",
"time"
],
"support": {
"docs": "https://carbon.nesbot.com/docs",
"issues": "https://github.com/briannesbitt/Carbon/issues",
"source": "https://github.com/briannesbitt/Carbon"
},
"funding": [
{
"url": "https://github.com/sponsors/kylekatarnls",
"type": "github"
},
{
"url": "https://opencollective.com/Carbon#sponsor",
"type": "opencollective"
},
{
"url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
"type": "tidelift"
}
],
"time": "2024-11-07T17:46:48+00:00"
},
{
"name": "nest/cli",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://code.zeptotech.net/Nest/Cli",
"reference": "79669be111783f9968f0a0ba77eba63a6495be96"
},
"require": {
"nest/core": "dev-main",
"php": "^8.3"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1-dev"
}
},
"autoload": {
"psr-4": {
"Nest\\Cli\\": "src/"
}
},
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"description": "Nest CLI service and engine.",
"time": "2024-11-08T14:56:14+00:00"
},
{
"name": "nest/core",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://code.zeptotech.net/Nest/Core",
"reference": "22296b8d7c8b528089189a8d9500395424cb6468"
},
"require": {
"php": "^8.3"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Nest\\": "src/"
},
"files": [
"src/Utils/Array.php",
"src/Utils/Paths.php",
"src/Utils/Reflection.php",
"src/Utils/String.php"
]
},
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"description": "Nest framework core.",
"time": "2024-11-08T11:12:38+00:00"
},
{
"name": "nest/events",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://code.zeptotech.net/Nest/Events",
"reference": "fcb3e114a7f9bdbcfd90f9283dd2c2588da09590"
},
"require": {
"nest/core": "dev-main",
"php": "^8.3"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Nest\\Events\\": "src/"
}
},
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"description": "Nest events implementation.",
"time": "2024-11-08T14:59:15+00:00"
},
{
"name": "nest/types",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://code.zeptotech.net/Nest/Types",
"reference": "b18a91301c76bd8e85a1c5de2f8960108aaa55ec"
},
"require": {
"nesbot/carbon": "^3.8",
"nest/core": "dev-main",
"php": "^8.3"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Nest\\Types\\": "src/"
}
},
"authors": [
{
"name": "Madeorsk",
"email": "madeorsk@protonmail.com"
}
],
"description": "Nest types implementation.",
"time": "2024-11-08T15:30:45+00:00"
},
{
"name": "psr/clock",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/clock.git",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Psr\\Clock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for reading the clock.",
"homepage": "https://github.com/php-fig/clock",
"keywords": [
"clock",
"now",
"psr",
"psr-20",
"time"
],
"support": {
"issues": "https://github.com/php-fig/clock/issues",
"source": "https://github.com/php-fig/clock/tree/1.0.0"
},
"time": "2022-11-25T14:36:26+00:00"
},
{
"name": "symfony/clock",
"version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/clock.git",
"reference": "97bebc53548684c17ed696bc8af016880f0f098d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d",
"reference": "97bebc53548684c17ed696bc8af016880f0f098d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"psr/clock": "^1.0",
"symfony/polyfill-php83": "^1.28"
},
"provide": {
"psr/clock-implementation": "1.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/now.php"
],
"psr-4": {
"Symfony\\Component\\Clock\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Decouples applications from the system clock",
"homepage": "https://symfony.com",
"keywords": [
"clock",
"psr20",
"time"
],
"support": {
"source": "https://github.com/symfony/clock/tree/v7.1.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-php83",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php83.git",
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491",
"reference": "2fb86d65e2d424369ad2905e83b236a8805ba491",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php83\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
"reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-uuid": "*"
},
"suggest": {
"ext-uuid": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Uuid\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Pineau",
"email": "lyrixx@lyrixx.info"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for uuid functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"uuid"
],
"support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/translation",
"version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/b9f72ab14efdb6b772f85041fa12f820dee8d55f",
"reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0"
},
"conflict": {
"symfony/config": "<6.4",
"symfony/console": "<6.4",
"symfony/dependency-injection": "<6.4",
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<6.4",
"symfony/service-contracts": "<2.5",
"symfony/twig-bundle": "<6.4",
"symfony/yaml": "<6.4"
},
"provide": {
"symfony/translation-implementation": "2.3|3.0"
},
"require-dev": {
"nikic/php-parser": "^4.18|^5.0",
"psr/log": "^1|^2|^3",
"symfony/config": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/finder": "^6.4|^7.0",
"symfony/http-client-contracts": "^2.5|^3.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/routing": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"files": [
"Resources/functions.php"
],
"psr-4": {
"Symfony\\Component\\Translation\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/translation/tree/v7.1.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-28T12:35:13+00:00"
},
{
"name": "symfony/translation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Translation\\": ""
},
"exclude-from-classmap": [
"/Test/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to translation",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/uid",
"version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
"reference": "65befb3bb2d503bbffbd08c815aa38b472999917"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/uid/zipball/65befb3bb2d503bbffbd08c815aa38b472999917",
"reference": "65befb3bb2d503bbffbd08c815aa38b472999917",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-uuid": "^1.15"
},
"require-dev": {
"symfony/console": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Uid\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Pineau",
"email": "lyrixx@lyrixx.info"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an object-oriented API to generate and represent UIDs",
"homepage": "https://symfony.com",
"keywords": [
"UID",
"ulid",
"uuid"
],
"support": {
"source": "https://github.com/symfony/uid/tree/v7.1.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"nest/cli": 20,
"nest/core": 20,
"nest/events": 20,
"nest/types": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^8.3",
"ext-pdo": "*"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View file

@ -0,0 +1,43 @@
<?php
namespace Nest\Cli\Commands\Migrations;
use Nest\Application;
use Nest\Cli\Commands\CommandHandler;
use Nest\Cli\Out;
use Nest\Database\Migrations\Migration;
use Nest\Database\Exceptions\Migrations\MigrationNotFoundException;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use Nest\Database\Exceptions\UnknownDatabaseException;
use Nest\Model\Exceptions\MissingRequiredFieldException;
use Nest\Types\Exceptions\IncompatibleTypeException;
use Throwable;
class MigrateCommand extends CommandHandler
{
/**
* @throws Throwable
* @throws MigrationNotFoundException
* @throws IncompatibleTypeException
* @throws UnknownDatabaseException
* @throws MissingConditionValueException
* @throws MissingRequiredFieldException
*/
public function __invoke(): void
{
Application::get()->migrations()->onMigrationStart = function (Migration $migration) {
Out::print("Executing migration V{$migration->getVersion()} ".Out::BOLD_ATTRIBUTE."{$migration->getName()}".Out::RESET_ATTRIBUTES);
echo " ".Out::ITALIC_ATTRIBUTE.$migration->getDescription().Out::RESET_ATTRIBUTES."\n\n";
};
Application::get()->migrations()->onMigrationEnd = function (Migration $migration) {
echo "\n";
Out::print("V{$migration->getVersion()}_{$migration->getName()} finished.");
echo "\n";
};
// Execute all remaining migrations.
Application::get()->migrations()->load()->migrate();
Out::success("Migrations executed successfully.");
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Nest\Cli\Commands\Migrations;
use Nest\Application;
use Nest\Cli\Commands\CommandHandler;
use Nest\Cli\Out;
use Nest\Database\Migrations\Migration;
use Nest\Database\Exceptions\Migrations\MigrationNotFoundException;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use Nest\Database\Exceptions\UnknownDatabaseException;
use Nest\Model\Exceptions\MissingRequiredFieldException;
use Nest\Types\Exceptions\IncompatibleTypeException;
use Throwable;
class MigrateOneCommand extends CommandHandler
{
/**
* @throws Throwable
* @throws MigrationNotFoundException
* @throws IncompatibleTypeException
* @throws UnknownDatabaseException
* @throws MissingConditionValueException
* @throws MissingRequiredFieldException
*/
public function __invoke(string $migrationId): void
{
Application::get()->migrations()->onMigrationStart = function (Migration $migration) {
Out::print("Executing migration V{$migration->getVersion()} ".Out::BOLD_ATTRIBUTE."{$migration->getName()}".Out::RESET_ATTRIBUTES);
echo " ".Out::ITALIC_ATTRIBUTE.$migration->getDescription().Out::RESET_ATTRIBUTES."\n\n";
};
Application::get()->migrations()->onMigrationEnd = function (Migration $migration) {
echo "\n";
Out::print("V{$migration->getVersion()}_{$migration->getName()} finished.");
echo "\n";
};
// Try to execute given migration.
Application::get()->migrations()->load()->migrateOne($migrationId);
Out::success("Success.");
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Nest\Cli\Commands\Migrations;
use Nest\Cli\Cli;
use Nest\Cli\Commands\CommandDefinition;
use Nest\Cli\Commands\ParameterDefinition;
use Nest\Cli\Exceptions\Command\InvalidCommandHandlerException;
use Nest\Cli\Exceptions\IncompatibleCliHandlerSubcommands;
/**
* Migrations commands definition manager.
*/
class MigrationsCommands
{
/**
* @param Cli $cli The CLI in which to define migrations commands.
* @return void
* @throws IncompatibleCliHandlerSubcommands
* @throws InvalidCommandHandlerException
*/
public static function define(Cli $cli): void
{
$cli->command("migrations")
->description("Migrations manager.")
->subcommands([
"migrate" => fn (CommandDefinition $subcommand) => $subcommand
->description("Execute all remaining migrations.")
->handler(MigrateCommand::class)
,
"migrateOne" => fn (CommandDefinition $subcommand) => $subcommand
->description("Perform one migration.")
->parameter("migrationId", fn (ParameterDefinition $parameter) => $parameter->description("Migration ID of migration to execute."))
->handler(MigrateOneCommand::class)
,
"rollback" => fn (CommandDefinition $subcommand) => $subcommand
->description("Rollback latest migration.")
->handler(RollbackCommand::class)
,
"new" => fn (CommandDefinition $subcommand) => $subcommand
->description("Generate a migration with the given name.")
->parameter("name", fn (ParameterDefinition $parameter) => $parameter->description("Name of the new migration."))
->handler(NewCommand::class)
,
]);
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Nest\Cli\Commands\Migrations;
use Nest\Application;
use Nest\Cli\Commands\CommandHandler;
use Nest\Cli\Out;
class NewCommand extends CommandHandler
{
public function __invoke(string $name): void
{
// Create a new migration.
Application::get()->migrations()->newMigration($name);
Out::success("Migration successfully created.");
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Nest\Cli\Commands\Migrations;
use Nest\Application;
use Nest\Cli\Commands\CommandHandler;
use Nest\Cli\Out;
use Nest\Database\Migrations\Migration;
use Nest\Database\Exceptions\Migrations\MigrationNotFoundException;
use Throwable;
class RollbackCommand extends CommandHandler
{
/**
* @throws MigrationNotFoundException
* @throws Throwable
*/
public function __invoke(): void
{
Application::get()->migrations()->onRollbackStart = function (Migration $migration) {
Out::print("Starting to roll back V{$migration->getVersion()} ".Out::BOLD_ATTRIBUTE."{$migration->getName()}".Out::RESET_ATTRIBUTES);
echo " ".Out::ITALIC_ATTRIBUTE.$migration->getDescription().Out::RESET_ATTRIBUTES."\n\n";
};
// Rollback latest migration.
$migration = Application::get()->migrations()->load()->rollbackLatest();
if (!empty($migration))
Out::success("Rolled back V{$migration->getVersion()} {$migration->getName()}");
else
Out::warning("Nothing to rollback.");
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Nest\Database\Configuration;
use Nest\Database\Database;
use Nest\Database\Exceptions\Configuration\MissingRequiredConfigurationValueException;
/**
* Create a database object from a configuration array.
*/
abstract class DatabaseFactory
{
/**
* @param string $databaseIdentifier Database identifier.
* @param array $configuration Database configuration array.
*/
public function __construct(protected string $databaseIdentifier, protected array $configuration)
{}
/**
* Get a required configuration value.
* @param string $configKey Configuration key.
* @return mixed Configuration value.
* @throws MissingRequiredConfigurationValueException
*/
public function getRequiredConfig(string $configKey): mixed
{
if (!isset($this->configuration[$configKey]))
throw new MissingRequiredConfigurationValueException($this->databaseIdentifier, static::class, $configKey);
return $this->configuration[$configKey];
}
/**
* Get an optional configuration value.
* @param string $configKey Configuration key.
* @param mixed|null $default Default configuration value.
* @return mixed Configuration value.
*/
public function getOptionalConfig(string $configKey, mixed $default = null): mixed
{
return $this->configuration[$configKey] ?? $default;
}
/**
* Make the database object from its configuration array.
* @return Database The created database.
* @throws MissingRequiredConfigurationValueException
*/
public abstract function make(): Database;
}

View file

@ -0,0 +1,40 @@
<?php
namespace Nest\Database\Configuration;
use Nest\Application;
use Nest\Configuration\Exceptions\ConfigurationValueNotFoundException;
use Nest\Database\Exceptions\Configuration\UndefinedDatabaseTypeException;
class DatabasesArrayConfiguration extends DatabasesConfiguration
{
/**
* @param Application $application The application.
* @param string $configurationKey Configuration key where to find the databases configuration array.
*/
public function __construct(protected Application $application, protected string $configurationKey)
{}
/**
* @inheritDoc
* @throws ConfigurationValueNotFoundException
* @throws UndefinedDatabaseTypeException
*/
#[\Override] public function getFactories(): array
{
// Initialize loaded factories.
$factories = [];
foreach ($this->application->configuration()->getValue($this->configurationKey) as $identifier => $configuration)
{
if (empty($configuration["type"]))
// Undefined database type, throwing an exception.
throw new UndefinedDatabaseTypeException($identifier);
// Create the factory with the given configuration.
$factories[$identifier] = new $configuration["type"]($identifier, $configuration);
}
return $factories; // Return loaded factories.
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Nest\Database\Configuration;
use Nest\Services\ServiceConfiguration;
abstract class DatabasesConfiguration extends ServiceConfiguration
{
/**
* Get database factories.
* @return array<string, DatabaseFactory> Database factories, indexed by databases identifiers.
*/
public abstract function getFactories(): array;
}

144
src/Database.php Normal file
View file

@ -0,0 +1,144 @@
<?php
namespace Nest\Database;
use Nest\Database\Query\QueryBuilder;
use Nest\Database\Transactions\Transaction;
use Nest\Database\Exceptions\NotCurrentTransactionException;
use Throwable;
/**
* Class of a database.
*/
abstract class Database
{
/**
* The current transaction, in one is started.
* @var Transaction|null
*/
private ?Transaction $transaction = null;
/**
* Connect to the database.
* @return void
*/
public abstract function connect(): void;
/**
* Execute a query.
* @param string $statement The query statement.
* @param array $bindings The query bindings.
* @return object[] Query result as an array of objects.
*/
public abstract function execute(string $statement, array $bindings = []): array;
/**
* Get a queries adapter for the current database.
* @return DatabaseAdapter A database queries adapter.
*/
public abstract function getQueriesAdapter(): DatabaseAdapter;
/**
* Create a new query on the database.
* @param ?string $table The table on which to build the new query.
* @return QueryBuilder A query builder.
*/
public function query(?string $table = null): QueryBuilder
{
return new QueryBuilder($this, $table);
}
/**
* Create a new query on the given table of the database.
* @param string $table The table on which to build the new query.
* @return QueryBuilder A query builder.
*/
public function table(string $table): QueryBuilder
{
return $this->query($table);
}
/**
* Get the current transaction, if there is one.
* @return Transaction|null
*/
public function getCurrentTransaction(): ?Transaction
{
return $this->transaction;
}
/**
* Called when the state of a transaction changed (inactive -> active or active -> inactive).
* @param Transaction $transaction
* @return void
* @throws NotCurrentTransactionException
*/
public function onTransactionStateChanged(Transaction $transaction): void
{
if ($transaction->isActive())
{ // Set the newly active transaction as the current one.
// Its parent should be the current transaction, if it exists.
if (!empty($transaction->getParent()) && $transaction->getParent()->getUuid() != $this->transaction->getUuid())
throw new NotCurrentTransactionException($this->transaction, $transaction->getParent());
$this->transaction = $transaction;
}
else
{ // The transaction has been terminated, it should be the current one.
if (!empty($this->transaction) && $this->transaction->getUuid() != $transaction->getUuid())
throw new NotCurrentTransactionException($this->transaction, $transaction);
$this->transaction = $transaction->getParent();
}
}
/**
* Start a new transaction (in the current one, if there is one).
* @return Transaction The new transaction.
* @throws NotCurrentTransactionException
*/
public function startTransaction(): Transaction
{
// Create the transaction as a child of the existing one, if there is one.
$newTransaction = new Transaction($this, $this->transaction);
if (!empty($this->transaction))
// Set the new transaction as child of the current one.
$this->transaction->setChild($newTransaction);
$newTransaction->start(); // Start the new transaction.
return $newTransaction; // Return the new transaction.
}
/**
* Start a new transaction and execute the function in parameter.
* The transaction will be committed / rolled back automatically at the end of the function.
* A rollback happen if any exception is thrown in the function.
* @param callable(Transaction): mixed $function The function to execute in a transaction.
* @return mixed Function result.
* @throws Throwable
*/
public function transaction(callable $function): mixed
{
// Start a new transaction.
$transaction = $this->startTransaction();
try
{ // Trying to run the function in the started transaction.
$result = $function($transaction);
// The function finished successfully, commit the transaction.
$transaction->commit();
// Return result of the function.
return $result;
}
catch (Throwable $throwable)
{ // An exception has been thrown, rolling back the transaction and throw it again.
$transaction->rollback();
throw $throwable;
}
}
}

199
src/DatabaseAdapter.php Normal file
View file

@ -0,0 +1,199 @@
<?php
namespace Nest\Database;
use Nest\Database\Migrations\Diff\TableColumn;
use Nest\Database\Migrations\Diff\TableForeignKey;
use Nest\Database\Migrations\Diff\TableIndex;
use Nest\Database\Query\Join\JoinBuilder;
use Nest\Database\Query\Raw;
use Nest\Database\Query\Where\ConditionBuilder;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
/**
* A database queries adapter.
*/
abstract class DatabaseAdapter
{
/**
* @param Database $database The database on which to execute the queries.
*/
public function __construct(protected Database $database)
{}
/**
* Start a new transaction.
* @return void
*/
public abstract function newTransaction(): void;
/**
* Rollback the current transaction.
* @return void
*/
public abstract function rollbackTransaction(): void;
/**
* Commit the current transaction.
* @return void
*/
public abstract function commitTransaction(): void;
/**
* Perform a table creation.
* @param string $tableName Name of the table to create.
* @param bool $ifNotExists True to try to add if table does not exist, will just pass if it exists.
* @return void
*/
public abstract function createTable(string $tableName, bool $ifNotExists = false): void;
/**
* Perform a table deletion.
* @param string $tableName Name of the table to drop.
* @param bool $ifExists True to try to drop if table exists, will just pass if it does not exist.
* @return void
*/
public abstract function dropTable(string $tableName, bool $ifExists = false): void;
/**
* Rename a table.
* @param string $tableName Table to rename.
* @param string $newTableName New table name.
* @return void
*/
public abstract function renameTable(string $tableName, string $newTableName): void;
/**
* Add a column to a table.
* @param TableColumn $tableColumn The column to add to a table.
* @param bool $ifNotExists True to try to add if column does not exist, will just pass if it exists.
* @return void
*/
public abstract function addTableColumn(TableColumn $tableColumn, bool $ifNotExists = false): void;
/**
* Set index of a table column.
* @param TableColumn $tableColumn The table column with index data.
* @return void
*/
public abstract function setTableColumnIndex(TableColumn $tableColumn): void;
/**
* Rename a table column.
* @param TableColumn $tableColumn The table column to rename.
* @param string $newName New name of the column.
* @return void
*/
public abstract function renameTableColumn(TableColumn $tableColumn, string $newName): void;
/**
* Modify a table colum.
* @param TableColumn $tableColumn The column to modify.
* @return void
*/
public abstract function alterTableColumn(TableColumn $tableColumn): void;
/**
* Drop a table column.
* @param string $tableName The table on which to drop a column.
* @param string $columnName The column name to drop.
* @param bool $ifExists True to try to drop if column exists, will just pass if it does not exist.
* @return void
*/
public abstract function dropTableColumn(string $tableName, string $columnName, bool $ifExists = false): void;
/**
* Drop a table constraint.
* @param string $tableName The table on which to drop a constraint.
* @param string $constraintName The constraint name to drop.
* @param bool $ifExists True to try to drop if constraint exists, will just pass if it does not exist.
* @return void
*/
public abstract function dropConstraint(string $tableName, string $constraintName, bool $ifExists = false): void;
/**
* Create a foreign key constraint.
* @param TableForeignKey $foreignKey The table foreign key to create.
* @return void
*/
public abstract function createForeignKey(TableForeignKey $foreignKey): void;
/**
* Create a table index.
* @param TableIndex $index The table index to create.
* @param bool $ifNotExists True to try to add if index does not exist, will just pass if it exists.
* @return void
*/
public abstract function createIndex(TableIndex $index, bool $ifNotExists = false): void;
/**
* Rename an existing index.
* @param string $indexName The table index to rename.
* @param string $newName The new name of the index.
* @param bool $ifExists True to try to rename if index exists, will just pass if it does not exist.
* @return void
*/
public abstract function renameIndex(string $indexName, string $newName, bool $ifExists = false): void;
/**
* Drop a table index.
* @param TableIndex $index The table index to drop.
* @param bool $ifExists True to try to drop if index exists, will just pass if it does not exist.
* @return void
*/
public abstract function dropIndex(TableIndex $index, bool $ifExists = false): void;
/**
* Build a SELECT query.
* @param string $tableName The table from which to select.
* @param (Raw|string)[] $selected Selected columns from the table.
* @param JoinBuilder[] $joins Joins of the query.
* @param ConditionBuilder[] $wheres Conditions applied to rows for selection.
* @param int|null $limit Limit of retrieved results. NULL by default = no limit.
* @return Raw SQL and its bindings.
* @throws MissingConditionValueException
*/
public abstract function buildSelect(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): Raw;
/**
* Select rows from a table.
* @param string $tableName The table from which to select.
* @param (Raw|string)[] $selected Selected columns from the table.
* @param JoinBuilder[] $joins Joins of the query.
* @param ConditionBuilder[] $wheres Conditions applied to rows for selection.
* @param int|null $limit Limit of retrieved results. NULL by default = no limit.
* @return object[] Selected rows objects.
* @throws MissingConditionValueException
*/
public abstract function select(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): array;
/**
* Insert new rows in a table.
* @param string $tableName The table where to insert.
* @param string[] $columns Columns of the table that are provided in rows values.
* @param array<string, Raw|string|int|float|null> $rows Rows values.
* @param bool $returning True to return inserted objects.
* @return object[]|bool Inserted rows objects if returning is true, true otherwise.
*/
public abstract function insert(string $tableName, array $columns, array $rows, bool $returning = false): array|bool;
/**
* Update rows of a table.
* @param string $tableName The table to update.
* @param array<string, Raw|string|int|float|null> $set Set values in the rows.
* @param ConditionBuilder[] $wheres Conditions applied to rows for update.
* @param bool $returning True to return updated objects.
* @return object[]|bool Updated rows objects if returning is true, true otherwise.
* @throws MissingConditionValueException
*/
public abstract function update(string $tableName, array $set, array $wheres, bool $returning = false): array|bool;
/**
* Delete rows from a table.
* @param string $tableName The table in which to delete rows.
* @param array $wheres Conditions applied to rows for deletion.
* @return void
* @throws MissingConditionValueException
*/
public abstract function delete(string $tableName, array $wheres): void;
}

57
src/Databases.php Normal file
View file

@ -0,0 +1,57 @@
<?php
namespace Nest\Database;
use Nest\Application;
use Nest\Database\Configuration\DatabasesConfiguration;
use Nest\Database\Exceptions\Configuration\MissingRequiredConfigurationValueException;
use Nest\Database\Exceptions\UnknownDatabaseException;
/**
* Databases manager.
*/
class Databases
{
/**
* Loaded databases.
* @var array<string, Database>
*/
private array $databases;
/**
* @param Application $application Application of the databases.
* @param DatabasesConfiguration $configuration Configuration of the databases.
* @throws MissingRequiredConfigurationValueException
*/
public function __construct(protected Application $application, protected DatabasesConfiguration $configuration)
{
$this->load();
}
/**
* Load databases from their factories.
* @return void
* @throws MissingRequiredConfigurationValueException
*/
public function load(): void
{
foreach ($this->configuration->getFactories() as $identifier => $databaseFactory)
{ // Load all databases from their factories.
$this->databases[$identifier] = $databaseFactory->make();
}
}
/**
* @param string $identifier Database identifier.
* @return Database Database corresponding to the given identifier.
* @throws UnknownDatabaseException
*/
public function db(string $identifier): Database
{
if (empty($this->databases[$identifier]))
// Cannot find a database with this identifier, throwing an exception.
throw new UnknownDatabaseException($identifier);
return $this->databases[$identifier]; // Return loaded database.
}
}

39
src/DatabasesService.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace Nest\Database;
use Nest\Database\Configuration\DatabasesConfiguration;
use Nest\Database\Exceptions\Configuration\MissingRequiredConfigurationValueException;
use Nest\Exceptions\Services\Configuration\UndefinedServiceConfigurationException;
/**
* Databases Nest service.
*/
trait DatabasesService
{
/**
* The databases manager.
* @var Databases
*/
private Databases $databases;
/**
* @return void
* @throws MissingRequiredConfigurationValueException
* @throws UndefinedServiceConfigurationException
*/
protected function __nest__DatabasesService(): void
{
// Initialize databases manager.
$this->databases = new Databases($this, $this->getServiceConfiguration(DatabasesConfiguration::class));
}
/**
* Databases service.
* @return Databases The databases manager.
*/
public function databases(): Databases
{
return $this->databases;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Nest\Database\Events;
use Nest\Database\PdoDatabase;
use Nest\Events\Event;
use PDO;
class PdoDatabaseAfterConnectionEvent extends Event
{
public function __construct(public PdoDatabase $database, public PDO $pdo)
{}
}

View file

@ -0,0 +1,12 @@
<?php
namespace Nest\Database\Events;
use Nest\Database\PdoDatabase;
use Nest\Events\Event;
class PdoDatabaseBeforeConnectionEvent extends Event
{
public function __construct(public PdoDatabase $database)
{}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Nest\Database\Exceptions\Configuration;
use Nest\Database\Exceptions\DatabaseException;
use Throwable;
/**
* Thrown when database loading fails because of a missing required configuration.
*/
class MissingRequiredConfigurationValueException extends DatabaseException
{
/**
* @param string $databaseIdentifier Database identifier.
* @param string $factoryClass Database factory class.
* @param string $configKey Missing configuration key.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $databaseIdentifier, public string $factoryClass, public string $configKey, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Cannot load database $this->databaseIdentifier: cannot find required configuration $this->configKey for factory $this->factoryClass.", $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\Exceptions\Configuration;
use Nest\Database\Exceptions\DatabaseException;
use Throwable;
/**
* Exception thrown when a database type is missing in one of the configurations.
*/
class UndefinedDatabaseTypeException extends DatabaseException
{
/**
* @param string $identifier Database identifier with missing type.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $identifier, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("", $code, $previous);
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Nest\Database\Exceptions;
use Nest\Exceptions\Exception;
class DatabaseException extends Exception
{
}

View file

@ -0,0 +1,14 @@
<?php
namespace Nest\Database\Exceptions;
/**
* Exception thrown when the current transaction does not allow to commit or rollback the transaction.
*/
class InvalidTransactionStateException extends DatabaseException
{
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Database\Exceptions\Migrations;
use Nest\Database\Migrations\Migration;
use Nest\Database\Exceptions\DatabaseException;
use Throwable;
/**
* Thrown when a migration cannot be rolled back.
*/
class CannotRollbackException extends DatabaseException
{
/**
* @param Migration $migration The migration which cannot be rolled back.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public Migration $migration, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Cannot rollback migration {$this->migration->getName()}.", $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\Exceptions\Migrations;
use Nest\Database\Exceptions\DatabaseException;
use Throwable;
/**
* Thrown when a given migration was not found.
*/
class MigrationNotFoundException extends DatabaseException
{
/**
* @param string $id Migration ID that wasn't found.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $id, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Migration $this->id not found.", $code, $previous);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Database\Exceptions\Migrations;
use Nest\Database\Exceptions\DatabaseException;
use Throwable;
/**
* Exception thrown when a new table column has an undefined type.
*/
class UndefinedNewColumnTypeException extends DatabaseException
{
/**
* @param string $tableName Table name.
* @param string $columnName Column name.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $tableName, public string $columnName, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Undefined column type for \"$this->tableName\".\"$this->columnName\".", $code, $previous);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Nest\Database\Exceptions;
use Nest\Database\Transactions\Transaction;
use Throwable;
/**
* Exception thrown when the altered transaction is not the current one.
*/
class NotCurrentTransactionException extends DatabaseException
{
/**
* @param Transaction $currentTransaction The current transaction.
* @param Transaction $alteredTransaction The altered transaction.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public Transaction $currentTransaction, public Transaction $alteredTransaction, int $code = 0, ?Throwable $previous = null)
{
parent::__construct(
"Only the current transaction (".$this->currentTransaction->getUuid().") can be altered. Cannot alter ".$this->alteredTransaction->getUuid().".",
$code, $previous
);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Nest\Database\Exceptions\Query;
use Throwable;
/**
* Exception thrown when no condition value has been given to the condition builder.
*/
class MissingConditionValueException extends QueryBuilderException
{
/**
* @param string $incompleteSql Incomplete SQL condition.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $incompleteSql, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Cannot complete SQL condition: $this->incompleteSql", $code, $previous);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\Exceptions\Query;
use Nest\Model\Entity;
use Throwable;
/**
* Exception thrown when an entity has no primary fields to use in WHERE clause.
*/
class NoPrimaryFieldException extends QueryBuilderException
{
/**
* @param Entity $entity The entity without primary fields.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public Entity $entity, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("No defined primary fields for ".get_class($this->entity).", cannot apply WHERE clause on it.", $code, $previous);
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace Nest\Database\Exceptions\Query;
use Nest\Database\Exceptions\DatabaseException;
class QueryBuilderException extends DatabaseException
{
}

View file

@ -0,0 +1,21 @@
<?php
namespace Nest\Database\Exceptions;
use Throwable;
/**
* Exception thrown when a requested database is unknown.
*/
class UnknownDatabaseException extends DatabaseException
{
/**
* @param string $identifier Database identifier.
* @param int $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
*/
public function __construct(public string $identifier, int $code = 0, ?Throwable $previous = null)
{
parent::__construct("Unknown database $this->identifier.", $code, $previous);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Nest\Database\Migrations\Configuration;
use Nest\Services\ServiceConfiguration;
use function Nest\Utils\path_join;
/**
* Migrations configuration class.
*/
abstract class MigrationsConfiguration extends ServiceConfiguration
{
/**
* @return string Database migrations PHP namespace.
*/
public abstract function getMigrationsNamespace(): string;
/**
* @return string Database migrations path.
*/
public abstract function getMigrationsPath(): string;
/**
* @return string Database preparations path.
*/
public function getPreparationsPath(): string
{
return path_join($this->getMigrationsPath(), "Prepare");
}
/**
* Get database to use for migrations table.
* @return string Database identifier.
*/
public function getMigrationsDatabase(): string
{
return "default";
}
/**
* @return string Done migrations table name.
*/
public function getMigrationsTable(): string
{
return "_migrations";
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Nest\Database\Migrations\Configuration;
/**
* Migrations configurator.
*/
class MigrationsConfigurator extends MigrationsConfiguration
{
private string $migrationsNamespace;
private string $migrationsPath;
private string $preparationsPath;
private string $migrationsDatabase;
private string $migrationsTable;
/**
* @param string $migrationsNamespace Database migrations PHP namespace.
* @return $this
*/
public function setMigrationsNamespace(string $migrationsNamespace): static
{
$this->migrationsNamespace = $migrationsNamespace;
return $this;
}
/**
* @inheritDoc
*/
#[\Override] public function getMigrationsNamespace(): string
{
return $this->migrationsNamespace;
}
/**
* @param string $migrationsPath Database migrations path.
* @return $this
*/
public function setMigrationsPath(string $migrationsPath): static
{
$this->migrationsPath = $migrationsPath;
return $this;
}
/**
* @inheritDoc
*/
#[\Override] public function getMigrationsPath(): string
{
return $this->migrationsPath;
}
/**
* @param string $preparationsPath Database preparations path.
* @return $this
*/
public function setPreparationsPath(string $preparationsPath): static
{
$this->preparationsPath = $preparationsPath;
return $this;
}
/**
* @inheritDoc
*/
#[\Override] public function getPreparationsPath(): string
{
return $this->preparationsPath ?? parent::getPreparationsPath();
}
/**
* Set database to use for migrations table.
* @param string $databaseIdentifier Database identifier.
* @return $this
*/
public function setMigrationsDatabase(string $databaseIdentifier): static
{
$this->migrationsDatabase = $databaseIdentifier;
return $this;
}
/**
* @inheritDoc
*/
public function getMigrationsDatabase(): string
{
return $this->migrationsDatabase ?? parent::getMigrationsDatabase();
}
/**
* @param string $migrationsTable Done migrations table name.
* @return $this
*/
public function setMigrationsTable(string $migrationsTable): static
{
$this->migrationsTable = $migrationsTable;
return $this;
}
/**
* @inheritDoc
*/
#[\Override] public function getMigrationsTable(): string
{
return $this->migrationsTable ?? parent::getMigrationsTable();
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Nest\Database\Migrations\Diff;
use Nest\Database\Database;
/**
* Table modifications manager.
*/
class Table
{
/**
* @param Database $database The database on which to perform operations on the given table.
* @param string $tableName Name of the table to modify.
*/
public function __construct(protected Database $database, protected string $tableName)
{}
/**
* The database on which to perform operations on the table.
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Get table name.
* @return string
*/
public function getTableName(): string
{
return $this->tableName;
}
/**
* Create the table.
* @param bool $ifNotExists True to try to add if table does not exist, will just pass if it exists.
* @return $this
*/
public function create(bool $ifNotExists = false): static
{
$this->getDatabase()->getQueriesAdapter()->createTable($this->getTableName(), $ifNotExists);
return $this;
}
/**
* Create the table, if it does not exists.
* @return $this
*/
public function createIfNotExists(): static
{
return $this->create(true);
}
/**
* Drop the table.
* @param bool $ifExists True to try to drop if table exists, will just pass if it does not exist.
* @return $this
*/
public function drop(bool $ifExists = false): static
{
$this->getDatabase()->getQueriesAdapter()->dropTable($this->getTableName(), $ifExists);
return $this;
}
/**
* Drop the table, if it exists.
* @return $this
*/
public function dropIfExists(): static
{
return $this->drop(true);
}
/**
* Rename the table.
* @param string $newName New name of the table.
* @return void
*/
public function rename(string $newName): void
{
$this->getDatabase()->getQueriesAdapter()->renameTable($this->getTableName(), $newName);
}
/**
* Get or create a column for the current table.
* @param string $columnName Column name.
* @return TableColumn The table column.
*/
public function column(string $columnName): TableColumn
{
return new TableColumn($this, $columnName);
}
/**
* Make a foreign key creator for the current table.
* @return TableForeignKey The table foreign key creator.
*/
public function foreignKey(): TableForeignKey
{
return new TableForeignKey($this);
}
/**
* Get or create an index for the current table.
* @param string $indexName Index name.
* @return TableIndex The table index.
*/
public function index(string $indexName): TableIndex
{
return new TableIndex($this, $indexName);
}
/**
* Drop a constraint with the given name.
* @param string $constraintName The constraint to drop.
* @param bool $ifExists True to try to drop if constraint exists, will just pass if it does not exist.
* @return void
*/
public function dropConstraint(string $constraintName, bool $ifExists = false): void
{
$this->getDatabase()->getQueriesAdapter()->dropConstraint($this->getTableName(), $constraintName, $ifExists);
}
/**
* Drop a constraint with the given name if it exists.
* @param string $constraintName The constraint to drop.
* @return void
*/
public function dropConstraintIfExists(string $constraintName): void
{
$this->dropConstraint($constraintName, true);
}
}

View file

@ -0,0 +1,188 @@
<?php
namespace Nest\Database\Migrations\Diff;
use BackedEnum;
use Nest\Database\Query\Raw;
use Nest\Database\Exceptions\Migrations\UndefinedNewColumnTypeException;
use Stringable;
/**
* Table column class.
*/
class TableColumn
{
/**
* SQL type of the column.
* @var string
*/
public string $type;
/**
* Column index definition.
* @var TableColumnIndex
*/
public TableColumnIndex $index;
/**
* Column is a primary key.
* @var bool
*/
public bool $primary;
/**
* Default column value.
* @var Raw|string|int|float|null
*/
public Raw|string|int|float|null $default;
/**
* Is column nullable or not.
* @var bool
*/
public bool $nullable;
/**
* Create a new table column.
* @param Table $table Table of the column.
* @param string $name Column name.
*/
public function __construct(public readonly Table $table, public readonly string $name)
{
}
/**
* Set the SQL type of the column.
* @param string|Stringable|BackedEnum $type SQL type of the column.
* @return $this
*/
public function type(string|Stringable|BackedEnum $type): static
{
$this->type = (string) ($type instanceof BackedEnum ? $type->value : $type);
return $this;
}
/**
* Set default column value.
* @param Raw|string|int|float|null $value Default column value.
* @return $this
*/
public function default(Raw|string|int|float|null $value): static
{
$this->default = $value;
return $this;
}
/**
* Set default column value to NOW.
* @return $this
*/
public function defaultNow(): static
{
return $this->default(new Raw("NOW()"));
}
/**
* Set nullable for the current column.
* @param bool $nullable True if the column is nullable, false otherwise.
* @return $this
*/
public function nullable(bool $nullable = true): static
{
$this->nullable = $nullable;
return $this;
}
/**
* Current column is a primary key.
* @return $this
*/
public function primary(): static
{
// Set the current column as a primary key.
$this->primary = true;
return $this;
}
/**
* Index the current column.
* @param bool $unique True to ensure a unique value for each row, false otherwise.
* @param string|null $method Custom index method (raw SQL!).
* @return $this
*/
public function index(bool $unique = false, ?string $method = null): static
{
// Initialize the column index.
$this->index = new TableColumnIndex();
$this->index->unique = $unique;
if (!empty($method)) $this->index->method = $method;
return $this;
}
/**
* Unique index the current column.
* @param string|null $method Custom index method (raw SQL!).
* @return $this
*/
public function unique(?string $method = null): static
{
// Initialize the column index.
return $this->index(true, $method);
}
/**
* Add the current column to the table.
* @param bool $ifNotExists True to try to add if column does not exist, will just pass if it exists.
* @return void
* @throws UndefinedNewColumnTypeException
*/
public function add(bool $ifNotExists = false): void
{
if (empty($this->type))
// Throw an exception if no type is provided.
throw new UndefinedNewColumnTypeException($this->table->getTableName(), $this->name);
// Add table column.
$this->table->getDatabase()->getQueriesAdapter()->addTableColumn($this, $ifNotExists);
// Set column index, if it's defined.
if (!empty($this->index))
$this->table->getDatabase()->getQueriesAdapter()->setTableColumnIndex($this);
}
/**
* Rename the column.
* @param string $newName New name of the column.
* @return void
*/
public function rename(string $newName): void
{
// Rename table column.
$this->table->getDatabase()->getQueriesAdapter()->renameTableColumn($this, $newName);
}
/**
* Alter the current table column.
* @return void
*/
public function alter(): void
{
// Alter table column.
$this->table->getDatabase()->getQueriesAdapter()->alterTableColumn($this);
// Set column index, if it's defined.
if (!empty($this->index))
$this->table->getDatabase()->getQueriesAdapter()->setTableColumnIndex($this);
}
/**
* Drop the current column from the table.
* @param bool $ifExists True to try to drop if column exists, will just pass if it does not exist.
* @return void
*/
public function drop(bool $ifExists = false): void
{
// Drop table column.
$this->table->getDatabase()->getQueriesAdapter()->dropTableColumn($this->table->getTableName(), $this->name, $ifExists);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Nest\Database\Migrations\Diff;
/**
* Table column index definition.
*/
class TableColumnIndex
{
/**
* Unique key index.
* @var bool
*/
public bool $unique = false;
/**
* Custom index method (raw SQL!).
* @var string
*/
public string $method;
}

View file

@ -0,0 +1,102 @@
<?php
namespace Nest\Database\Migrations\Diff;
/**
* Table foreign key configurator.
*/
class TableForeignKey
{
/**
* Foreign key columns.
* @var string[]
*/
public array $columns = [];
/**
* Referenced table.
* @var string
*/
public string $referencedTable;
/**
* Referenced columns.
* @var string[]
*/
public array $referencedColumns;
/**
* Action on delete.
* @var string
*/
public string $onDelete;
/**
* Action on update.
* @var string
*/
public string $onUpdate;
/**
* Create a new table foreign key.
* @param Table $table Table of the foreign key.
*/
public function __construct(public readonly Table $table)
{
}
/**
* Add foreign key columns.
* @param string ...$columns Foreign key columns.
* @return $this
*/
public function columns(string ...$columns): static
{
array_push($this->columns, ...$columns);
return $this;
}
/**
* Set referenced table and columns..
* @param string $referencedTable Referenced table name.
* @param string[] $referencedColumns Referenced columns in table.
* @return $this
*/
public function references(string $referencedTable, string ...$referencedColumns): static
{
$this->referencedTable = $referencedTable;
$this->referencedColumns = $referencedColumns;
return $this;
}
/**
* Set action on delete.
* @param string $referentialAction Raw action.
* @return $this
*/
public function onDelete(string $referentialAction): static
{
$this->onDelete = $referentialAction;
return $this;
}
/**
* Set action on update.
* @param string $referentialAction Raw action.
* @return $this
*/
public function onUpdate(string $referentialAction): static
{
$this->onUpdate = $referentialAction;
return $this;
}
/**
* Create the defined foreign key.
* @return void
*/
public function create(): void
{
$this->table->getDatabase()->getQueriesAdapter()->createForeignKey($this);
}
}

View file

@ -0,0 +1,167 @@
<?php
namespace Nest\Database\Migrations\Diff;
/**
* Table index class.
*/
class TableIndex
{
/**
* Index columns.
* @var string[]
*/
public array $columns;
/**
* Raw expression to index.
* @var string|null
*/
public ?string $rawExpression = null;
/**
* Whether indexed values must be uniques or not.
* @var bool
*/
public bool $unique = false;
/**
* Index method to use.
* @var string|null
*/
public ?string $method = null;
/**
* ASC or DESC.
* @var string|null
*/
public ?string $order = null;
/**
* NULLS FIRST or NULLS LAST.
* @var string|null
*/
public ?string $nulls = null;
/**
* Create a new table index.
* @param Table $table Table of the index.
* @param string $name Index name.
*/
public function __construct(public readonly Table $table, public readonly string $name)
{
}
/**
* Add columns in the index.
* @param string ...$columns Columns to add in the index.
* @return $this
*/
public function columns(string ...$columns): static
{
array_push($this->columns, ...$columns);
return $this;
}
/**
* Set a raw expression to index.
* @param string $rawExpression Raw expression to index.
* @return $this
*/
public function expression(string $rawExpression): static
{
$this->rawExpression = $rawExpression;
return $this;
}
/**
* Set that indexed values must be uniques.
* @param bool $unique True to set unique, false to set non-unique.
* @return $this
*/
public function unique(bool $unique = true): static
{
$this->unique = $unique;
return $this;
}
/**
* Set the method to used to index.
* @param string|null $method Index method to use.
* @return $this
*/
public function using(?string $method): static
{
$this->method = $method;
return $this;
}
/**
* Ascending sort order.
* @return $this
*/
public function asc(): static
{
$this->order = "ASC";
return $this;
}
/**
* Descending sort order.
* @return $this
*/
public function desc(): static
{
$this->order = "DESC";
return $this;
}
/**
* Nulls sort before non-nulls.
* @return $this
*/
public function nullsFirst(): static
{
$this->nulls = "NULLS FIRST";
return $this;
}
/**
* Nulls sort after non-nulls.
* @return $this
*/
public function nullsLast(): static
{
$this->nulls = "NULLS LAST";
return $this;
}
/**
* Create index.
* @param bool $ifNotExists True to try to add if index does not exist, will just pass if it exists.
* @return void
*/
public function create(bool $ifNotExists = false): void
{
$this->table->getDatabase()->getQueriesAdapter()->createIndex($this, $ifNotExists);
}
/**
* Rename index.
* @param string $newName The new name of the index.
* @param bool $ifExists True to try to rename if index exists, will just pass if it does not exist.
* @return void
*/
public function renameTo(string $newName, bool $ifExists = false): void
{
$this->table->getDatabase()->getQueriesAdapter()->renameIndex($this->name, $newName, $ifExists);
}
/**
* Drop index.
* @param bool $ifExists True to try to drop if index exists, will just pass if it does not exist.
* @return void
*/
public function drop(bool $ifExists = false): void
{
$this->table->getDatabase()->getQueriesAdapter()->dropIndex($this, $ifExists);
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Nest\Database\Migrations;
use Nest\Database\Database;
use Nest\Database\Migrations\Diff\Table;
/**
* A database migration.
*/
abstract class Migration
{
/**
* Construct a new database migration.
* @param Database $database The database of the migration.
* @param string $version Version identifier.
* @param string $name Database migration name.
*/
public function __construct(protected readonly Database $database, private readonly string $version, private readonly string $name)
{
}
/**
* @return string The version identifier.
*/
public function getVersion(): string
{
return $this->version;
}
/**
* @return string The database migration name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Describes operations of the current database migration.
* @return string Description of database migration.
*/
public abstract function getDescription(): string;
/**
* Do the migration.
* @return void
*/
public abstract function up(): void;
/**
* Undo the migration.
* @return void
*/
public abstract function down(): void;
/**
* Create a new table diff manager.
* @param string $tableName Table name to alter.
* @return Table Table diff manager.
*/
protected function table(string $tableName): Table
{
return new Table($this->database, $tableName);
}
}

View file

@ -0,0 +1,572 @@
<?php
namespace Nest\Database\Migrations;
use Nest\Application;
use Nest\Database\Database;
use Nest\Database\Migrations\Configuration\MigrationsConfiguration;
use DateTime;
use Nest\Database\Migrations\Diff\Table;
use Nest\Database\PostgreSql\Columns\Timestamp;
use Nest\Database\PostgreSql\Columns\Type;
use Nest\Database\Exceptions\Migrations\MigrationNotFoundException;
use Nest\Database\Exceptions\Migrations\UndefinedNewColumnTypeException;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use Nest\Database\Exceptions\UnknownDatabaseException;
use Nest\Exceptions\InvalidTypeException;
use Nest\Model\Exceptions\MissingRequiredFieldException;
use Nest\Model\Exceptions\UndefinedRelationException;
use Nest\Model\Exceptions\UnhandledPropertyTypeException;
use Nest\Model\Exceptions\IncompatibleTypeException;
use Nest\Model\EntityBlueprint;
use Throwable;
use function Nest\Utils\path_join;
class Migrations
{
/**
* The database on which to execute the migrations.
* @var Database
*/
protected Database $database;
/**
* The migrations array.
* Indexed by their version identifier.
* @var array<string, array>
*/
private array $migrations = [];
/**
* The preparations scripts array.
* @var string[]
*/
private array $preparations = [];
/**
* Migration start event.
* @var callable(Migration): void|null
*/
public $onMigrationStart = null;
/**
* Migration end event.
* @var callable(Migration): void|null
*/
public $onMigrationEnd = null;
/**
* Rollback start event.
* @var callable(Migration): void|null
*/
public $onRollbackStart = null;
/**
* Rollback end event.
* @var callable(Migration): void|null
*/
public $onRollbackEnd = null;
/**
* @param Application $application The application.
* @param MigrationsConfiguration $configuration The migrations configuration.
*/
public function __construct(protected Application $application, protected MigrationsConfiguration $configuration)
{
}
/**
* Initial load of the migrations system.
* @return $this
* @throws IncompatibleTypeException
* @throws MissingConditionValueException
* @throws UnknownDatabaseException
* @throws UndefinedNewColumnTypeException
*/
public function load(): static
{
// Get migrations database.
$this->database = $this->application->databases()->db($this->configuration->getMigrationsDatabase());
// Initial load of database migrations and preparations.
$this->prepareMigrationsTable();
$this->loadMigrations();
$this->loadPreparations();
return $this;
}
/**
* Try to register a new migration file.
* @param string $migrationFile The migration file name.
* @param string $subdirectory Subdirectory where the migration file has been found.
* @return void
*/
protected function registerMigration(string $migrationFile, string $subdirectory): void
{
if (preg_match("/^V([0-9]+)_([^.]+)\\.php$/", $migrationFile, $matches))
{ // The filename matches migration name format.
$version = $matches[1];
$name = $matches[2];
$this->migrations[$version] = [
"version" => $version,
"name" => $name,
"migrated" => false, // Already run migrations are found by using findMigrated after migration registration.
"entity" => null, // Migration entity in database, when it's migrated.
"package" => $subdirectory,
];
}
}
/**
* Recursively load migrations in the given path with subdirectory.
* @param string $migrationsPath The path where to find migrations.
* @param string $subdirectory The subdirectory / subpackage of registered migration files.
* @return void
*/
private function _loadMigrations(string $migrationsPath, string $subdirectory = ""): void
{
// Get the list of files in the migrations directory.
$migrationFiles = scandir($migrationsPath);
foreach ($migrationFiles as $migrationFile)
{ // Try to register each migration file.
$migrationFileFullPath = path_join($migrationsPath, $migrationFile);
if (is_dir($migrationFileFullPath) && $migrationFile != "." && $migrationFile != "..")
{ // If it is a directory, we try to read it recursively.
$this->_loadMigrations($migrationFileFullPath, !empty($subdirectory) ? path_join($subdirectory, $migrationFile) : $migrationFile);
}
else
{ // It is a file, we try to register it as a migration file.
$this->registerMigration($migrationFile, $subdirectory);
}
}
}
/**
* Load all existing migrations.
* @return void
* @throws IncompatibleTypeException
* @throws MissingConditionValueException
*/
protected function loadMigrations(): void
{
// Reset migrations list.
$this->migrations = [];
// Load migrations in the configured migrations path.
$this->_loadMigrations($this->configuration->getMigrationsPath());
// Sort migrations by lexicographic order of version identifiers.
ksort($this->migrations);
// Find done migrations.
$this->findMigrated();
}
/**
* Load all preparations scripts.
* @return void
*/
protected function loadPreparations(): void
{
// Reset preparations scripts list.
$this->preparations = [];
if (file_exists($this->configuration->getPreparationsPath()))
{ // If there is a preparation folder.
// Get all preparations scripts files.
$this->preparations = scandir($this->configuration->getPreparationsPath());
// Filter non-SQL files.
$this->preparations = array_filter($this->preparations, fn ($preparationScript) => (
// Only keep files ending with ".sql".
is_file($preparationScript) && str_ends_with($preparationScript, ".sql")
));
// Sort in lexicographic order.
sort($this->preparations);
}
}
/**
* Prepare migrations table: create it if it doesn't exists.
* @return void
* @throws UndefinedNewColumnTypeException
*/
protected function prepareMigrationsTable(): void
{
$table = new Table($this->database, $this->configuration->getMigrationsTable());
$table->createIfNotExists();
$table->column("id")->type(Type::Varchar)->primary()->add(true);
$table->column("name")->type(Type::Varchar)->index()->add(true);
$table->column("created_at")->type(new Timestamp(true))->defaultNow()->add(true);
}
/**
* Get a migration model for the current migrations.
* @return \Nest\Database\Migrations\Model\Migration
*/
protected function getMigrationModel(): \Nest\Database\Migrations\Model\Migration
{
// Create an anonymous class for the current migrations.
$migrationModel = new class extends \Nest\Database\Migrations\Model\Migration
{
/**
* Migrations table name.
* @var string
*/
public static string $migrationsTableName;
/**
* @inheritDoc
*/
public function definition(EntityBlueprint $blueprint): EntityBlueprint
{
if (!empty(static::$migrationsTableName))
// Set migrations table name.
$blueprint->setTable(static::$migrationsTableName);
return parent::definition($blueprint);
}
};
// Set migrations table name.
// $migrationModel cannot be used as is: the table name has been defined after the first definition.
$migrationModel::$migrationsTableName = $this->configuration->getMigrationsTable();
return $migrationModel->new(); // Return migrations model base instance.
}
/**
* Find done migrations.
* @return void
* @throws IncompatibleTypeException
* @throws MissingConditionValueException
* @throws UnknownDatabaseException
* @throws InvalidTypeException
* @throws UndefinedRelationException
* @throws UnhandledPropertyTypeException
*/
protected function findMigrated(): void
{
/**
* Get migrated migrations.
* @var \Nest\Database\Migrations\Model\Migration[] $migrations
*/
$migrations = $this->getMigrationModel()->query()->get();
foreach ($migrations as $migration)
{ // Set each migration as migrated.
$this->migrations[$migration->id]["migrated"] = true;
$this->migrations[$migration->id]["entity"] = $migration;
}
}
/**
* Mark the given migration as migrated.
* @param string $migrationId The migration version identifier.
* @return void
* @throws IncompatibleTypeException
* @throws MissingConditionValueException
* @throws UnknownDatabaseException
* @throws MissingRequiredFieldException
*/
public function markMigrated(string $migrationId): void
{
// Mark migration as migrated.
$this->migrations[$migrationId]["migrated"] = true;
// Create a migration and save it as migrated.
$this->migrations[$migrationId]["entity"] = $this->getMigrationModel()->new();
$this->migrations[$migrationId]["entity"]->id = $migrationId;
$this->migrations[$migrationId]["entity"]->name = $this->migrations[$migrationId]["name"];
$this->migrations[$migrationId]["entity"]->save();
}
/**
* Mark the given migration as NOT migrated.
* @param string $migrationId The migration version identifier.
* @return void
*/
public function markNotMigrated(string $migrationId): void
{
$this->migrations[$migrationId]["entity"]->delete();
$this->migrations[$migrationId]["migrated"] = false;
}
/**
* Get a new version for a new database migration.
* @return string The version of a new migration.
*/
protected function getNewVersion(): string
{
// Generate a new version from current date and time.
return (new DateTime())->format("YmdHis");
}
/**
* Generate a new migration with the given name.
* @param string $migrationName The migration name.
* @return void
*/
public function newMigration(string $migrationName): void
{
// Database migration filename.
$filename = path_join($this->configuration->getMigrationsPath(), "V".($version = $this->getNewVersion()))."_{$migrationName}.php";
// Generate new migration content.
file_put_contents($filename, <<<EOD
<?php
declare(strict_types=1);
namespace {$this->configuration->getMigrationsNamespace()};
use Nest\Database\Migrations\Migration;
final class V{$version}_{$migrationName} extends Migration
{
/**
* @inheritDoc
*/
#[\Override] public function getDescription(): string
{ return ""; }
/**
* @inheritDoc
*/
public function up(): void
{}
/**
* @inheritDoc
*/
public function down(): void
{
throw new \Nest\Exceptions\Database\Migrations\CannotRollbackException(\$this);
}
}
EOD
);
}
/**
* Get migrations versions list.
* @return string[]
*/
public function getMigrations(): array
{
return array_keys($this->migrations);
}
/**
* Construct a new migration instance from its version identifier.
* @param array $migrationData Migration data associative array.
* @return Migration The migration instance.
*/
protected function newMigrationInstance(array $migrationData): Migration
{
// Get migration full class name.
$fullClassName = "\\".$this->configuration->getMigrationsNamespace()
// Add subpackage if there is one.
.(!empty($migrationData["package"]) ? "\\".$migrationData["package"] : "")
// Add migration class name.
."\\V{$migrationData["version"]}_{$migrationData["name"]}";
// Instantiate migration.
return new $fullClassName($this->database, $migrationData["version"], $migrationData["name"]);
}
/**
* Execute a migration.
* @param string $migrationId The migration version identifier.
* @return Migration The executed migration instance.
* @throws IncompatibleTypeException
* @throws MigrationNotFoundException
* @throws MissingConditionValueException
* @throws MissingRequiredFieldException
* @throws UnknownDatabaseException
* @throws Throwable
*/
public function migrateOne(string $migrationId): Migration
{
if (empty($this->migrations[$migrationId]))
// Migration not found.
throw new MigrationNotFoundException($migrationId);
// Migrate in a transaction.
return $this->database->transaction(function () use ($migrationId) {
// Get the migration instance.
$migration = $this->newMigrationInstance($this->migrations[$migrationId]);
if (!empty($this->onMigrationStart))
// Fire migration start event, if it is defined.
($this->onMigrationStart)($migration);
// Execute migration.
$migration->up();
// Mark the migration as migrated.
$this->markMigrated($migrationId);
if (!empty($this->onMigrationEnd))
// Fire migration end event, if it is defined.
($this->onMigrationEnd)($migration);
// Return executed migration.
return $migration;
});
}
/**
* Execute all unapplied migrations.
* @return Migration[] The applied migrations list.
* @throws IncompatibleTypeException
* @throws MigrationNotFoundException
* @throws MissingConditionValueException
* @throws MissingRequiredFieldException
* @throws UnknownDatabaseException
* @throws Throwable
*/
public function migrate(): array
{
// Migrate in a transaction.
return $this->database->transaction(function () {
// Initialize the executed migrations.
$migrated = [];
// Clear preparations before executing migrations.
$this->clearPreparations();
foreach ($this->migrations as $version => $migration)
{ // For each migration, if it is not executed, we execute it.
if (!$migration["migrated"])
{ // If the migration isn't done, execute it.
$migrated[$version] = $this->migrateOne($version);
}
}
// Do database preparations after migrations.
$this->prepare();
return $migrated; // Return executed migrations list.
});
}
/**
* Rollback a migration.
* @param string $migrationId The migration version identifier.
* @return Migration The rolled back migration.
* @throws MigrationNotFoundException
* @throws Throwable
*/
public function rollbackOne(string $migrationId): Migration
{
if (empty($this->migrations[$migrationId]))
// Migration not found.
throw new MigrationNotFoundException($migrationId);
// Rollback in a transaction.
return $this->database->transaction(function () use ($migrationId) {
// Clear preparations before rolling back.
$this->clearPreparations();
// Get the migration instance.
$migration = $this->newMigrationInstance($this->migrations[$migrationId]);
if (!empty($this->onRollbackStart))
// Fire rollback start event, if it is defined.
($this->onRollbackStart)($migration);
// Rollback migration.
$migration->down();
// Mark the migration as not migrated.
$this->markNotMigrated($migrationId);
// Do database preparations after rollback.
$this->prepare();
if (!empty($this->onRollbackEnd))
// Fire rollback start event, if it is defined.
($this->onRollbackEnd)($migration);
return $migration; // Return rolled back migration.
});
}
/**
* Rollback the latest migrated migration
* @return Migration|null The rolled back migration, if there is one.
* @throws MigrationNotFoundException
* @throws Throwable
*/
public function rollbackLatest(): ?Migration
{
// Get latest migration ID.
$latestMigration = null;
foreach (array_reverse($this->migrations, true) as $migrationId => $migration)
{ // Exploring migrations in reverse order (the most recent first).
if ($migration["migrated"])
{ // The first migrated migration is taken as the latest one.
$latestMigration = $migrationId;
break;
}
}
if (!empty($latestMigration))
// Rollback the latest migrated migration.
return $this->rollbackOne($latestMigration);
else
// No latest migration, do nothing and return NULL.
return null;
}
/**
* The preparations clear scripts filename regex.
*/
const string PREPARATIONS_CLEAR_SCRIPT_REGEX = "/^0+_(.*)$/";
/**
* Clear database preparations.
* Database preparations are cleared before migrations and should drop all functions and views used by tables that can
* be changed by migrations.
* @return void
*/
public function clearPreparations(): void
{
foreach ($this->preparations as $preparationScript)
{ // Execute all preparations clear scripts.
if (preg_match(static::PREPARATIONS_CLEAR_SCRIPT_REGEX, $preparationScript))
// Only execute files with 00_ prefix.
$this->execPreparation($preparationScript);
}
}
/**
* Do database preparations.
* Database preparations are executed after migrations and can create functions and views based on tables.
* @return void
*/
public function prepare(): void
{
foreach ($this->preparations as $preparationScript)
{ // Execute all preparations that are not clear scripts.
if (!preg_match(static::PREPARATIONS_CLEAR_SCRIPT_REGEX, $preparationScript))
// Only execute files that aren't preparations clear scripts.
$this->execPreparation($preparationScript);
}
}
/**
* Execute a given preparation script.
* @param string $preparationScript The preparation script to execute.
* @return void
*/
private function execPreparation(string $preparationScript): void
{
// Read preparation script and execute it.
$this->database->execute(file_get_contents($preparationScript));
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Nest\Database\Migrations;
use Nest\Database\Migrations\Configuration\MigrationsConfiguration;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use Nest\Database\Exceptions\UnknownDatabaseException;
use Nest\Exceptions\Services\Configuration\UndefinedServiceConfigurationException;
use Nest\Types\Exceptions\IncompatibleTypeException;
/**
* Nest database migrations service.
*/
trait MigrationsService
{
/**
* The database migrations instance.
* @var Migrations
*/
private Migrations $migrations;
/**
* @throws UndefinedServiceConfigurationException
* @throws UnknownDatabaseException
* @throws MissingConditionValueException
* @throws IncompatibleTypeException
*/
protected function __nest__MigrationsService(): void
{
// Initialize database migrations instance.
$this->migrations = new Migrations($this, $this->getServiceConfiguration(MigrationsConfiguration::class));
}
/**
* Database migrations service.
* @return Migrations The database migrations manager.
*/
public function migrations(): Migrations
{
return $this->migrations;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Nest\Database\Migrations\Model;
use Carbon\Carbon;
use Nest\Model\Entity;
use Nest\Model\EntityBlueprint;
use Nest\Types\Definitions\StringType;
/**
* Class of an executed migration.
*/
class Migration extends Entity
{
/**
* Migration version ID.
* @var string
*/
public string $id;
/**
* Migration name.
* @var string
*/
public string $name;
/**
* Migration execution date.
* @var Carbon
*/
public Carbon $created_at;
/**
* @inheritDoc
*/
public function definition(EntityBlueprint $blueprint): EntityBlueprint
{
$blueprint->field("id", StringType::class)->primary();
$blueprint->field("name", StringType::class)->index();
$blueprint->createdAt();
return $blueprint;
}
}

111
src/PdoDatabase.php Normal file
View file

@ -0,0 +1,111 @@
<?php
namespace Nest\Database;
use Nest\Database\Events\PdoDatabaseAfterConnectionEvent;
use Nest\Database\Events\PdoDatabaseBeforeConnectionEvent;
use Nest\Events\HasEvents;
use PDO;
/**
* Class of a PDO database.
*/
abstract class PdoDatabase extends Database
{
use HasEvents;
/**
* The PDO connection to database.
* @var PDO
*/
protected PDO $pdo;
/**
* @return string The PDO DSN to connect to the database.
*/
protected abstract function getDsn(): string;
/**
* @return string The username to connect to the database.
*/
protected abstract function getUsername(): string;
/**
* @return string The password to connect to the database.
*/
protected abstract function getPassword(): string;
/**
* @return array PDO options.
* @see PDO::__construct
*/
protected function getPdoOptions(): array
{
return [
// Persistent connection by default.
PDO::ATTR_PERSISTENT => true,
PDO::ERRMODE_EXCEPTION => true,
];
}
/**
* @inheritDoc
*/
#[\Override] public function connect(): void
{
$this->getEventsManager()->fire(new PdoDatabaseBeforeConnectionEvent($this));
// Connect to the database using PDO.
$this->pdo = new PDO($this->getDsn(), $this->getUsername(), $this->getPassword(), $this->getPdoOptions());
$this->getEventsManager()->fire(new PdoDatabaseAfterConnectionEvent($this, $this->pdo));
}
/**
* @inheritDoc
*/
#[\Override] public function execute(string $statement, array $bindings = []): array
{
// Prepare the statement.
$statement = $this->getConnection()->prepare($statement);
foreach ($bindings as $param => $value)
{ // Assign bindings to the statement.
$statement->bindValue(is_int($param) ? $param + 1 : $param, $value);
}
// Exception is thrown in case of failure.
$statement->execute();
// Fetching results.
$result = [];
while ($obj = $statement->fetchObject())
{ // Fetch next object while there is one.
$result[] = $obj;
}
// Return results.
return $result;
}
/**
* Check if the connection to the database is active.
* @return bool True if the connection to the database is active, false otherwise.
*/
public function isConnected(): bool
{
return !empty($this->pdo);
}
/**
* Get the current connection to the database.
* @return PDO
*/
public function getConnection(): PDO
{
if (!$this->isConnected())
// If we are not connected, trying to connect to the database.
$this->connect();
return $this->pdo;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL fixed-length bit string column type.
*/
class Bit implements Stringable
{
/**
* @param int $size Size of the fixed-length bit string column type.
*/
public function __construct(protected int $size)
{}
public function __toString(): string
{
return "bit($this->size)";
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL fixed-length character string column type.
*/
class Char implements Stringable
{
/**
* @param int $size Size of the fixed-length character string column type.
*/
public function __construct(protected int $size)
{}
public function __toString(): string
{
return "char($this->size)";
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL numeric column type.
*/
class Numeric implements Stringable
{
/**
* @param int $precision Precision of the numeric column type.
* @param int $scale Scale of the numeric column type.
* @see https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL
*/
public function __construct(protected int $precision, protected int $scale = 0)
{}
public function __toString(): string
{
return "numeric($this->precision, $this->scale)";
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL time column type.
*/
class Time implements Stringable
{
/**
* @param bool $timezone Add timezone to the value.
*/
public function __construct(protected bool $timezone = false)
{}
public function __toString(): string
{
return "time" . ($this->timezone ? " with time zone" : "");
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL date + time column type.
*/
class Timestamp implements Stringable
{
/**
* @param bool $timezone Add timezone to the value.
*/
public function __construct(protected bool $timezone = false)
{}
public function __toString(): string
{
return "timestamp" . ($this->timezone ? " with time zone" : "");
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
/**
* PostgreSQL simple column types.
* See also parametric types:
* @see Bit
* @see Varbit
* @see Char
* @see Varchar
* @see Numeric
*/
enum Type: string
{
case Boolean = "boolean";
case SmallInt = "smallint";
case SmallSerial = "smallserial";
case Int = "int";
case Serial = "serial";
case BigInt = "bigint";
case BigSerial = "bigserial";
case Float = "float4";
case Double = "float8";
case Numeric = "numeric";
case Money = "money";
case Bit = "bit";
case Varbit = "varbit";
case ByteArray = "bytea";
case Char = "char";
case Varchar = "varchar";
case Text = "text";
case TextSearchQuery = "tsquery";
case TextSearchVector = "tsvector";
case Uuid = "uuid";
case Json = "json";
case JsonBinary = "jsonb";
case Xml = "xml";
case Date = "date";
case Time = "time";
case Timestamp = "timestamp";
case Interval = "interval";
case Cidr = "cidr";
case MacAddr = "macaddr";
case MacAddr8 = "macaddr8";
case Point = "point";
case Line = "line";
case Segment = "lseg";
case Path = "path";
case Polygon = "polygon";
case Box = "box";
case Circle = "circle";
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL variable-length bit string column type.
*/
class Varbit implements Stringable
{
/**
* @param int $size Size of the variable-length bit string column type.
*/
public function __construct(protected int $size)
{}
public function __toString(): string
{
return "varbit($this->size)";
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Nest\Database\PostgreSql\Columns;
use Stringable;
/**
* PostgreSQL variable-length character string column type.
*/
class Varchar implements Stringable
{
/**
* @param int $size Size of the variable-length character string column type.
*/
public function __construct(protected int $size)
{}
public function __toString(): string
{
return "varchar($this->size)";
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Database\PostgreSql;
use Nest\Database\Configuration\DatabaseFactory;
use Nest\Database\Database;
class PostgreSql extends DatabaseFactory
{
/**
* @inheritDoc
*/
#[\Override] public function make(): Database
{
return new PostgreSqlDatabase(
$this->getRequiredConfig("host"),
$this->getRequiredConfig("database"),
$this->getRequiredConfig("username"),
$this->getRequiredConfig("password"),
$this->getOptionalConfig("port", 5432),
);
}
}

View file

@ -0,0 +1,501 @@
<?php
namespace Nest\Database\PostgreSql;
use Nest\Database\DatabaseAdapter;
use Nest\Database\Migrations\Diff\TableColumn;
use Nest\Database\Migrations\Diff\TableForeignKey;
use Nest\Database\Migrations\Diff\TableIndex;
use Nest\Database\Query\Join\JoinBuilder;
use Nest\Database\Query\Raw;
use Nest\Database\Query\Where\ConditionBuilder;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use Override;
use function Nest\Utils\format_object_name;
/**
* PostgreSQL database queries adapter.
*/
class PostgreSqlAdapter extends DatabaseAdapter
{
/**
* @inheritDoc
*/
#[\Override] public function newTransaction(): void
{
$this->database->execute("BEGIN;");
}
/**
* @inheritDoc
*/
#[\Override] public function rollbackTransaction(): void
{
$this->database->execute("ROLLBACK;");
}
/**
* @inheritDoc
*/
#[\Override] public function commitTransaction(): void
{
$this->database->execute("COMMIT;");
}
/**
* @inheritDoc
*/
#[Override] public function createTable(string $tableName, bool $ifNotExists = false): void
{
$this->database->execute("CREATE TABLE ".($ifNotExists ? "IF NOT EXISTS " : "").format_object_name($tableName)."();");
}
/**
* @inheritDoc
*/
#[\Override] public function dropTable(string $tableName, bool $ifExists = false): void
{
$this->database->execute("DROP TABLE ".($ifExists ? "IF EXISTS " : "").format_object_name($tableName).";");
}
/**
* @inheritDoc
*/
#[\Override] public function renameTable(string $tableName, string $newTableName): void
{
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." RENAME TO ".format_object_name($newTableName).";");
}
/**
* @inheritDoc
*/
#[\Override] public function addTableColumn(TableColumn $tableColumn, bool $ifNotExists = false): void
{
// SQL base query.
$sql = "ADD COLUMN".($ifNotExists ? " IF NOT EXISTS" : "")." ".format_object_name($tableColumn->name)." $tableColumn->type";
// Set nullable or not.
if (!empty($tableColumn->nullable))
$sql .= " NULL";
else
$sql .= " NOT NULL";
// Set default value if there is one.
if (isset($tableColumn->default))
$sql .= " DEFAULT ".((string) $tableColumn->default);
if (!empty($tableColumn->primary))
$sql .= " PRIMARY KEY";
// Execute add table column.
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." {$sql};");
}
/**
* @inheritDoc
*/
#[\Override] public function setTableColumnIndex(TableColumn $tableColumn): void
{
// Create index name.
$indexName = $tableColumn->table->getTableName()."_".$tableColumn->name."_index";
// Drop existing index, if it exists.
$this->database->execute("DROP INDEX IF EXISTS {$indexName};");
// SQL base query.
$sql = "CREATE ".($tableColumn->index->unique ? "UNIQUE " : "")."INDEX $indexName ON ".format_object_name($tableColumn->table->getTableName());
// Set index method if specified.
if (!empty($tableColumn->index->method))
$sql .= " USING ({$tableColumn->index->method})";
// Set column name in new index.
$sql .= " ({$tableColumn->name})";
// Execute add column index.
$this->database->execute("$sql;");
}
/**
* @inheritDoc
*/
#[\Override] public function renameTableColumn(TableColumn $tableColumn, string $newName): void
{
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName()).
" RENAME COLUMN ".format_object_name($tableColumn->name).
" TO ".format_object_name($newName));
}
/**
* @inheritDoc
*/
#[\Override] public function alterTableColumn(TableColumn $tableColumn): void
{
// SQL base query.
$sql = "ALTER COLUMN ".format_object_name($tableColumn->name);
if (!empty($tableColumn->type))
$sql .= " TYPE $tableColumn->type";
// Set nullable or not.
if (isset($tableColumn->nullable))
{
if ($tableColumn->nullable)
$sql .= " DROP NOT NULL";
else
$sql .= " SET NOT NULL";
}
// Set default value if there is one.
if (isset($tableColumn->default))
$sql .= " SET DEFAULT ".((string) $tableColumn->default);
// Execute add table column.
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." {$sql};");
if (!empty($tableColumn->primary))
// Set the column as a primary key.
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." ADD PRIMARY KEY (".format_object_name($tableColumn->name).");");
}
/**
* @inheritDoc
*/
#[\Override] public function dropTableColumn(string $tableName, string $columnName, bool $ifExists = false): void
{
// SQL base query.
$sql = "DROP COLUMN ".($ifExists ? "IF EXISTS " : "").format_object_name($columnName);
// Execute drop table column.
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." {$sql};");
}
/**
* @inheritDoc
*/
#[\Override] public function dropConstraint(string $tableName, string $constraintName, bool $ifExists = false): void
{
// SQL base query.
$sql = "DROP CONSTRAINT ".($ifExists ? "IF EXISTS " : "").format_object_name($constraintName);
// Execute drop table constraint.
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." {$sql};");
}
/**
* @inheritDoc
*/
#[\Override] public function createForeignKey(TableForeignKey $foreignKey): void
{
// Format columns list.
$columns = implode(", ", array_map(fn ($column) => format_object_name($column), $foreignKey->columns));
// Format referenced columns list.
$referencedColumns = implode(", ", array_map(fn ($column) => format_object_name($column), $foreignKey->referencedColumns));
// Base SQL.
$sql = "ADD FOREIGN KEY ($columns) REFERENCES ".format_object_name($foreignKey->referencedTable)."($referencedColumns)";
// Set ON DELETE and ON UPDATE actions.
if (!empty($foreignKey->onDelete))
$sql .= " ON DELETE {$foreignKey->onDelete}";
if (!empty($foreignKey->onUpdate))
$sql .= " ON UPDATE {$foreignKey->onUpdate}";
// Execute add foreign key constraint.
$this->database->execute("ALTER TABLE ".format_object_name($foreignKey->table->getTableName())." {$sql};");
}
/**
* @inheritDoc
*/
#[\Override] public function createIndex(TableIndex $index, bool $ifNotExists = false): void
{
// Base SQL.
$sql = "CREATE (UNIQUE) INDEX".($ifNotExists ? " IF NOT EXISTS" : "")." $index->name ON ".format_object_name($index->table->getTableName());
if (!empty($index->method))
$sql .= " USING $index->method";
$sql .= "(";
if (!empty($index->rawExpression))
$sql .= "($index->rawExpression)";
else
{ // Format indexed columns.
// Format columns list.
$columns = implode(", ", array_map(fn ($column) => format_object_name($column), $index->columns));
$sql .= "($columns)";
}
// Set index order, if defined.
if (!empty($index->order))
$sql .= " $index->order";
if (!empty($index->nulls))
$sql .= " $index->nulls";
$sql .= ")";
// Execute index creation.
$this->database->execute("$sql;");
}
/**
* @inheritDoc
*/
#[\Override] public function renameIndex(string $indexName, string $newName, bool $ifExists = false): void
{
// Execute index rename.
$this->database->execute("ALTER INDEX".($ifExists ? "IF EXISTS " : "")." ".format_object_name($indexName).
" RENAME TO ".format_object_name($newName).";");
}
/**
* @inheritDoc
*/
#[\Override] public function dropIndex(TableIndex $index, bool $ifExists = false): void
{
// Execute index drop.
$this->database->execute("DROP INDEX".($ifExists ? "IF EXISTS " : "")." ".format_object_name($index->name).";");
}
/**
* Build JOIN clauses from join builders.
* @param JoinBuilder[] $joins Joins to build.
* @return array{string, array} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
* @throws MissingConditionValueException
*/
protected function buildJoins(array $joins): array
{
// Return empty join clauses if there are no conditions.
if (empty($joins)) return ["", []];
$sql = "";
$bindings = [];
// Build all join clauses.
foreach ($joins as $join)
{ // For each join clause, build its conditions and append the built bindings.
[$onSql, $onBindings] = $this->buildWhere($join->getConditions(), "ON");
$sql .= "$join->type JOIN \"$join->table\" $onSql ";
array_push($bindings, ...$onBindings);
}
// Return all joins clauses SQL and bindings.
return [$sql, $bindings];
}
/**
* Build WHERE clause from conditions builders.
* @param ConditionBuilder[] $wheres Conditions to build.
* @param string $keyword The keyword to use. "WHERE" by default, but can be replaced by "ON" for JOIN conditions.
* @return array{string, array} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
* @throws MissingConditionValueException
*/
protected function buildWhere(array $wheres, string $keyword = "WHERE"): array
{
// Return empty where clause if there are no conditions.
if (empty($wheres)) return ["", []];
// Build all conditions.
$conditions = array_map(fn (ConditionBuilder $condition) => $condition->toSql(), $wheres);
// Join all conditions with AND.
$sql = implode("AND", array_column($conditions, 0));
// Join all values bindings.
$bindings = array_merge(...array_column($conditions, 1));
// Return built WHERE clause.
return ["$keyword $sql", $bindings];
}
/**
* @inheritDoc
*/
#[\Override] public function buildSelect(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): Raw
{
$bindings = []; // Initialize empty bindings.
// SQL base query: select the table columns.
$sql = "SELECT ".implode(", ", array_map(function (Raw|string|array $select) use (&$bindings) {
if ($select instanceof Raw)
{ // Raw selection.
// Add its bindings to the list.
array_push($bindings, ...$select->bindings);
// Return raw SQL.
return (string) $select;
}
else
// Format selection and return its SQL.
return format_object_name((string) $select);
}, $selected));
// Append FROM clause.
$sql .= " FROM ".format_object_name($tableName);
// Append JOIN clauses.
[$joinsSql, $joinsBindings] = $this->buildJoins($joins);
if (!empty($joinsSql))
{
$sql .= " $joinsSql";
array_push($bindings, ...$joinsBindings);
}
// Append WHERE clause.
[$whereSql, $whereBindings] = $this->buildWhere($wheres);
if (!empty($whereSql))
{
$sql .= " $whereSql";
array_push($bindings, ...$whereBindings);
}
// Append LIMIT clause.
if (!is_null($limit))
$sql .= " LIMIT $limit";
// Return raw SQL with its bindings.
return new Raw($sql, $bindings);
}
/**
* @inheritDoc
*/
#[\Override] public function select(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): array
{
// Build SELECT query.
$raw = $this->buildSelect($tableName, $selected, $joins, $wheres, $limit);
// Execute built query and return result.
return $this->database->execute($raw->sql, $raw->bindings);
}
/**
* @inheritDoc
*/
#[\Override] public function insert(string $tableName, array $columns, array $rows, bool $returning = false): array|bool
{
// SQL base query: insert into the table columns.
$sql = "INSERT INTO ".format_object_name($tableName).
" (".implode(", ", array_map(fn (string $column) => format_object_name($column), $columns)).")";
// Add values (with bindings).
$sql .= " VALUES ";
$rowsValues = [];
$bindings = [];
foreach ($rows as $row)
{ // Create a new VALUES tuple for each row.
// Initialize row parts of the current row.
$rowParts = [];
$rowBindings = [];
foreach ($row as $value)
{ // For each row value, add a row part corresponding to its type.
if ($value instanceof Raw)
// Add a new raw row part.
$rowParts[] = (string) $value;
else
{ // Add a new row part, with its binding.
$rowParts[] = "?";
$rowBindings[] = $value;
}
}
// Append current row tuple to values, with its bindings.
$rowsValues[] = "(".implode(", ", $rowParts).")";
array_push($bindings, ...$rowBindings);
}
// Build values SQL and append it to the query.
$sql .= implode(", ", $rowsValues);
if ($returning)
// Inserted rows shall be returned, indicating it in the query.
$sql .= "RETURNING *";
// SQL query end.
$sql .= ";";
// Execute INSERT query.
$result = $this->database->execute($sql, $bindings);
if ($returning)
// Returning inserted rows.
return $result;
else
// INSERT executed successfully.
return true;
}
/**
* @inheritDoc
*/
#[\Override] public function update(string $tableName, array $set, array $wheres, bool $returning = false): array|bool
{
// SQL base query: update the given table.
$sql = "UPDATE ".format_object_name($tableName);
$bindings = []; // Initialize empty bindings.
// Initialize SET clause.
$sql .= " SET ";
$setParts = [];
foreach ($set as $columnName => $value)
{ // For each set value, add a set part corresponding to its type.
$currentSet = format_object_name($columnName)." = ";
if ($value instanceof Raw)
// Add a new raw set part.
$setParts[] = $currentSet.((string) $value);
else
{ // Add a new set part, with its binding.
$setParts[] = $currentSet."?";
$bindings[] = $value;
}
}
// Append SET columns.
$sql .= implode(", ", $setParts);
// Append WHERE clause.
[$whereSql, $whereBindings] = $this->buildWhere($wheres);
if (!empty($whereSql))
{
$sql .= " $whereSql";
array_push($bindings, ...$whereBindings);
}
if ($returning)
// Inserted rows shall be returned, indicating it in the query.
$sql .= "RETURNING *";
// SQL query end.
$sql .= ";";
// Execute UPDATE query.
$result = $this->database->execute($sql, $bindings);
if ($returning)
// Returning updated rows.
return $result;
else
// UPDATE executed successfully.
return true;
}
/**
* @inheritDoc
*/
#[\Override] public function delete(string $tableName, array $wheres): void
{
// SQL base query: delete from the given table.
$sql = "DELETE FROM ".format_object_name($tableName);
// Append WHERE clause.
[$whereSql, $bindings] = $this->buildWhere($wheres);
if (!empty($whereSql))
$sql .= " $whereSql";
// SQL query end.
$sql .= ";";
// Execute DELETE query.
$this->database->execute($sql, $bindings);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Nest\Database\PostgreSql;
use Nest\Database\DatabaseAdapter;
use Nest\Database\PdoDatabase;
class PostgreSqlDatabase extends PdoDatabase
{
/**
* @param string $host Database host or unix socket directory path.
* @param string $database Database name.
* @param string $username Username to use to connect to the database.
* @param string $password Password to use to connect to the database.
* @param int $port Database port.
*/
public function __construct(protected string $host, protected string $database, protected string $username, protected string $password, protected int $port = 5432)
{}
/**
* @inheritDoc
*/
#[\Override] public function getQueriesAdapter(): DatabaseAdapter
{
return new PostgreSqlAdapter($this);
}
/**
* @inheritDoc
*/
#[\Override] protected function getDsn(): string
{
return "pgsql:host={$this->host};port={$this->port};dbname={$this->database}";
}
/**
* @inheritDoc
*/
#[\Override] protected function getUsername(): string
{
return $this->username;
}
/**
* @inheritDoc
*/
#[\Override] protected function getPassword(): string
{
return $this->password;
}
}

43
src/Query/DeleteQuery.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace Nest\Database\Query;
use Nest\Database\Database;
use Nest\Database\Query\Where\HasWhere;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
/**
* Builder of a DELETE query.
*/
class DeleteQuery
{
use HasWhere;
/**
* Create a new DELETE query.
* @param Database $database The database on which to execute the query.
* @param string $table Base table of the DELETE query.
*/
public function __construct(protected Database $database, protected string $table)
{}
/**
* Get the database on which to execute the query.
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Execute DELETE query.
* @return void
* @throws MissingConditionValueException
*/
public function execute(): void
{
// Execute built query and return result.
$this->database->getQueriesAdapter()->delete($this->table, $this->wheres);
}
}

75
src/Query/InsertQuery.php Normal file
View file

@ -0,0 +1,75 @@
<?php
namespace Nest\Database\Query;
use Nest\Database\Database;
use function Nest\Utils\array_unique_quick;
/**
* Builder of an INSERT query.
*/
class InsertQuery
{
/**
* Values to insert.
* @var array<array<string, Raw|string|int|float|null>>
*/
protected array $values = [];
/**
* Create a new INSERT query.
* @param Database $database The database on which to execute the query.
* @param string $table Base table of the INSERT query.
*/
public function __construct(protected Database $database, protected string $table)
{}
/**
* Reset values to insert.
* @return $this
*/
public function resetValues(): static
{
$this->values = [];
return $this;
}
/**
* Add values to insert.
* @param array<string, Raw|string|int|float|null> ...$row Each parameter is an associative array that represents one row data.
* @return $this
*/
public function values(array ...$row): static
{
// Append all rows to values array.
array_push($this->values, ...$row);
return $this;
}
/**
* Execute insert query.
* @param bool $returning True to return inserted objects.
* @return object[]|bool Inserted objects if returning is true, true otherwise.
*/
public function execute(bool $returning = false): array|bool
{
// Find all inserted columns.
$columns = array_unique_quick(array_merge(...array_map(fn (array $row) => array_keys($row), $this->values)));
// Prepare rows to insert (fill rows with DEFAULT values, if some of them are missing something).
$rows = array_map(function (array $rowValues) use ($columns) {
// Initialize current row.
$row = [];
foreach ($columns as $column)
{ // Get value for each column: from row values if it is defined, use DEFAULT otherwise.
$row[$column] = $rowValues[$column] ?? new Raw("DEFAULT");
}
return $row; // Return full row ready to insert.
}, $this->values);
// Perform insert.
return $this->database->getQueriesAdapter()->insert($this->table, $columns, $rows, $returning);
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Nest\Database\Query\Join;
/**
* Add join capacity to a query builder.
*/
trait HasJoin
{
/**
* Join clauses.
* @var JoinBuilder[]
*/
protected array $joins = [];
/**
* Reset JOIN clauses.
* @return $this
*/
public function resetJoins(): static
{
$this->joins = [];
return $this;
}
/**
* Create a new join clause.
* @param string $type Type of the join clause.
* @param string $table Joined table.
* @return JoinBuilder New join clause builder.
*/
public function join(string $type, string $table): JoinBuilder
{
$join = new JoinBuilder($this, $type, $table);
$this->joins[] = $join;
return $join;
}
/**
* Create a new inner join clause.
* @param string $table Joined table.
* @return JoinBuilder New join clause builder.
*/
public function innerJoin(string $table): JoinBuilder
{
return $this->join(JoinBuilder::INNER, $table);
}
/**
* Create a new outer join clause.
* @param string $table Joined table.
* @return JoinBuilder New join clause builder.
*/
public function outerJoin(string $table): JoinBuilder
{
return $this->join(JoinBuilder::OUTER, $table);
}
/**
* Create a new left join clause.
* @param string $table Joined table.
* @return JoinBuilder New join clause builder.
*/
public function leftJoin(string $table): JoinBuilder
{
return $this->join(JoinBuilder::LEFT, $table);
}
/**
* Create a new right join clause.
* @param string $table Joined table.
* @return JoinBuilder New join clause builder.
*/
public function rightJoin(string $table): JoinBuilder
{
return $this->join(JoinBuilder::RIGHT, $table);
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace Nest\Database\Query\Join;
use Nest\Database\Query\Where\ConditionBuilder;
class JoinBuilder
{
const string INNER = "INNER";
const string OUTER = "OUTER";
const string LEFT = "LEFT";
const string RIGHT = "RIGHT";
/**
* @param mixed $query The calling query.
* @param string $type Join type (INNER, OUTER, LEFT, RIGHT, ...).
* @param string $table The table to join.
*/
public function __construct(protected mixed $query, public readonly string $type, public readonly string $table)
{}
/**
* ON conditions of the JOIN clause.
* @var ConditionBuilder[]
*/
protected array $on = [];
/**
* Reset ON conditions.
* @return $this
*/
public function resetOn(): static
{
$this->on = [];
return $this;
}
/**
* Add a ON condition.
* @param string|callable $column The column of the condition, or a condition builder callable.
* @param string $operator The operator of the condition (or the value column if no value is passed).
* @param string|null $valueColumn The value column of the condition.
* @return mixed The query.
*/
public function on(string|callable $column, string $operator, string $valueColumn = null): mixed
{
if (is_callable($column))
{ // Callable condition builder.
$this->on[] = $column(new ConditionBuilder());
}
else
{ // Simple condition, registering it.
if (!empty($operator) && empty($valueColumn))
{ // If there are 2 parameters, considering the second one as the value, with default operator.
$valueColumn = $operator;
// Default operator: "=".
$operator = "=";
}
// Create the simple condition.
$this->on[] = (new ConditionBuilder())->column($column)->operator($operator)->column($valueColumn);
}
return $this->query;
}
/**
* Get join conditions.
* @return ConditionBuilder[] ON conditions of the JOIN clause.
*/
public function getConditions(): array
{
return $this->on;
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Nest\Database\Query;
use Nest\Database\Database;
/**
* Database query builder.
*/
class QueryBuilder
{
/**
* Base table of the query.
* @var string
*/
protected string $table;
/**
* Build a new query builder.
* @param Database $database The database on which to execute the query.
* @param string|null $table Base table of the query.
*/
public function __construct(protected Database $database, ?string $table = null)
{
if (!empty($table)) $this->setTable($table);
}
/**
* Get the database on which to execute the query.
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Set the table to use in the query.
* @param string $table The table to use in the query.
* @return $this
*/
public function setTable(string $table): static
{
$this->table = $table;
return $this;
}
/**
* Get the table to use in the query.
* @return string
*/
public function getTable(): string
{
return $this->table;
}
/**
* Create a new SELECT query.
* @return SelectQuery A select query.
*/
public function newSelect(): SelectQuery
{
return new SelectQuery($this->getDatabase(), $this->getTable());
}
/**
* Create a new INSERT query.
* @return InsertQuery An INSERT query.
*/
public function newInsert(): InsertQuery
{
return new InsertQuery($this->getDatabase(), $this->getTable());
}
/**
* Create a new UPDATE query.
* @return UpdateQuery An UPDATE query.
*/
public function newUpdate(): UpdateQuery
{
return new UpdateQuery($this->getDatabase(), $this->getTable());
}
/**
* Create a new DELETE query.
* @return DeleteQuery A DELETE query.
*/
public function newDelete(): DeleteQuery
{
return new DeleteQuery($this->getDatabase(), $this->getTable());
}
}

23
src/Query/Raw.php Normal file
View file

@ -0,0 +1,23 @@
<?php
namespace Nest\Database\Query;
/**
* Raw SQL content.
*/
readonly class Raw
{
/**
* Create new raw SQL content.
* @param string $sql Raw SQL content.
* @param array $bindings Raw SQL bindings.
*/
public function __construct(public string $sql, public array $bindings = [])
{
}
public function __toString(): string
{
return $this->sql;
}
}

135
src/Query/SelectQuery.php Normal file
View file

@ -0,0 +1,135 @@
<?php
namespace Nest\Database\Query;
use Nest\Database\Database;
use Nest\Database\Query\Join\HasJoin;
use Nest\Database\Query\Where\HasWhere;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
/**
* Builder of a SELECT query.
*/
class SelectQuery
{
use HasJoin;
use HasWhere;
/**
* Selected columns.
* @var (Raw|string)[]
*/
protected array $selected = [];
/**
* Limit of results to get.
* NULL = no limit.
* @var int|null
*/
protected ?int $limit = null;
/**
* Create a new SELECT query.
* @param Database $database The database on which to execute the query.
* @param string $table Base table of the SELECT query.
*/
public function __construct(protected Database $database, protected string $table)
{}
/**
* Get the database on which to execute the query.
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Get retrieved table name.
* @return string Selected table name.
*/
public function getTableName(): string
{
return $this->table;
}
/**
* Reset selected columns.
* @return $this
*/
public function resetSelect(): static
{
$this->selected = [];
return $this;
}
/**
* Add selected columns.
* @param string|Raw ...$selectedColumns Selected columns.
* @return $this
*/
public function select(string|Raw ...$selectedColumns): static
{
// Append SELECTed columns.
array_push($this->selected, ...$selectedColumns);
return $this;
}
/**
* Add raw selected SQL.
* @param string ...$rawSelect Raw selected SQL.
* @return $this
*/
public function selectRaw(string ...$rawSelect): static
{
// Append raw SQL SELECT.
array_push($this->selected, ...array_map(fn (string $rawSql) => new Raw($rawSql), $rawSelect));
return $this;
}
/**
* Set the new limit of results.
* @param int|null $limit Limit of retrieved results. NULL to remove the limit.
* @return $this
*/
public function limit(?int $limit): static
{
$this->limit = $limit;
return $this;
}
/**
* Build SELECT query.
* @return Raw SQL and its bindings.
* @throws MissingConditionValueException
*/
public function build(): Raw
{
// Build SELECT query and return its result.
return $this->database->getQueriesAdapter()->buildSelect($this->table,
// Select all columns by default.
empty($this->selected) ? [new Raw("*")] : $this->selected,
$this->joins,
$this->wheres,
$this->limit,
);
}
/**
* Execute SELECT query.
* @return object[] SELECT query result.
* @throws MissingConditionValueException
*/
public function execute(): array
{
// Execute built query and return result.
return $this->database->getQueriesAdapter()->select($this->table,
// Select all columns by default.
empty($this->selected) ? [new Raw("*")] : $this->selected,
$this->joins,
$this->wheres,
$this->limit,
);
}
}

80
src/Query/UpdateQuery.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace Nest\Database\Query;
use Nest\Database\Database;
use Nest\Database\Query\Where\HasWhere;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
/**
* Builder of an UPDATE query.
*/
class UpdateQuery
{
use HasWhere;
/**
* Set values.
* @var array<string, Raw|string|int|float|null>
*/
protected array $set = [];
/**
* Create a new UPDATE query.
* @param Database $database The database on which to execute the query.
* @param string $table Base table of the UPDATE query.
*/
public function __construct(protected Database $database, protected string $table)
{}
/**
* Get the database on which to execute the query.
* @return Database
*/
public function getDatabase(): Database
{
return $this->database;
}
/**
* Reset set values.
* @return $this
*/
public function resetSet(): static
{
$this->set = [];
return $this;
}
/**
* Add set columns values.
* @param string|array<string, Raw|string|int|float|null> $column The name of the column in column mode, or an associative array of all set columns with their corresponding value.
* @param Raw|string|int|float|null $value The value to set, when setting in column mode (with a column as first parameter).
* @return $this
*/
public function set(string|array $column, Raw|string|int|float|null $value = null): static
{
if (is_array($column))
{ // The first parameter is an array, adding set values in array mode.
$this->set = array_merge($this->set, $column);
}
else
{ // The first parameter is a string, adding set value in column mode.
$this->set[$column] = $value;
}
return $this;
}
/**
* Execute update query.
* @param bool $returning True to return inserted objects.
* @return object[]|bool Updated objects if returning is true, true otherwise.
* @throws MissingConditionValueException
*/
public function execute(bool $returning = false): array|bool
{
// Execute built query and return result.
return $this->database->getQueriesAdapter()->update($this->table, $this->set, $this->wheres, $returning);
}
}

View file

@ -0,0 +1,215 @@
<?php
namespace Nest\Database\Query\Where;
use Nest\Database\Exceptions\Query\MissingConditionValueException;
use function Nest\Utils\format_object_name;
class ConditionBuilder
{
protected string $column;
protected string $operator;
protected mixed $value;
protected string $valueColumn;
/**
* And conditions to append.
* @var ConditionBuilder[]
*/
protected array $ands = [];
/**
* Or conditions to append.
* @var ConditionBuilder[]
*/
protected array $ors = [];
/**
* Set column of the condition.
* When called a second time when there's no value, set the value column.
* @param string $column Column (or value column) to use.
* @return $this
*/
public function column(string $column): static
{
if (empty($this->column) || !empty($this->value))
$this->column = $column;
else
$this->valueColumn = $column;
return $this;
}
/**
* Set operator of the condition.
* @param string $operator Operator to use.
* @return $this
*/
public function operator(string $operator): static
{
$this->operator = $operator;
return $this;
}
/**
* Set value of the condition.
* @param mixed $value Value to use.
* @return $this
*/
public function value(mixed $value): static
{
$this->value = $value;
return $this;
}
/**
* Set "=" operator.
* @return $this
*/
public function equals(): static
{
return $this->operator("=");
}
/**
* Set "LIKE" operator.
* @return $this
*/
public function like(): static
{
return $this->operator("LIKE");
}
/**
* Set "EXISTS" operator.
* @return $this
*/
public function exists(): static
{
return $this->operator("EXISTS");
}
/**
* Append and conditions.
* @param callable ...$conditionBuilder And conditions builders.
* @return $this
*/
public function and(callable ...$conditionBuilder): static
{
foreach ($conditionBuilder as $conditionBuilderCallable)
{
$this->ands[] = $conditionBuilderCallable(new ConditionBuilder());
}
return $this;
}
/**
* Append or conditions.
* @param callable ...$conditionBuilder Or conditions builders.
* @return $this
*/
public function or(callable ...$conditionBuilder): static
{
foreach ($conditionBuilder as $conditionBuilderCallable)
{
$this->ors[] = $conditionBuilderCallable(new ConditionBuilder());
}
return $this;
}
/**
* Build SQL AND subcondition.
* @param ConditionBuilder $condition The subcondition builder.
* @return array{string, string[]} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
* @throws MissingConditionValueException
*/
private function buildAndSql(ConditionBuilder $condition): array
{
[$sql, $bindings] = $condition->toSql();
return ["AND $sql", $bindings];
}
/**
* Build SQL OR subcondition.
* @param ConditionBuilder $condition The subcondition builder.
* @return array{string, string[]} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
* @throws MissingConditionValueException
*/
private function buildOrSql(ConditionBuilder $condition): array
{
[$sql, $bindings] = $condition->toSql();
return ["OR $sql", $bindings];
}
/**
* Build SQL of the condition.
* @return array{string, string[]} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
* @throws MissingConditionValueException
*/
public function toSql(): array
{
// Build subconditions.
$subconditions = [
...array_map(
fn (ConditionBuilder $condition) => $this->buildAndSql($condition),
$this->ands,
), ...array_map(
fn (ConditionBuilder $condition) => $this->buildOrSql($condition),
$this->ors,
)
];
// SQL to append after current condition.
$appendSql = "";
// Values bindings to append after current condition.
$appendBindings = [];
foreach ($subconditions as [$sql, $bindings])
{ // Append current subcondition to SQL.
$appendSql .= $sql;
// Append current subcondition bindings.
array_push($appendBindings, ...$bindings);
}
if (!empty($this->column))
{ // If a column is defined, build the condition on it.
if (($this->operator ?? "=") === "EXISTS" || (empty($this->operator) && empty($this->value) && empty($this->valueColumn)))
// "EXISTS" operator, or no operator nor value are defined.
return ["(EXISTS ".format_object_name($this->column)." $appendSql)", $appendBindings];
else
{
if (!empty($this->value))
{ // Condition value is a bound value.
if (is_array($this->value))
{
$valueSql = "(".implode(",", array_fill(0, count($this->value), "?")).")";
$bindings = $this->value;
}
else
{
$valueSql = "?";
$bindings = [$this->value];
}
}
elseif (!empty($this->valueColumn))
{ // Condition value is an SQL column.
$valueSql = format_object_name($this->valueColumn);
$bindings = [];
}
else
// A condition value is missing.
throw new MissingConditionValueException("(".format_object_name($this->column)." $this->operator [...MISSING_CONDITION_VALUE...])");
// Default operator is "=".
$operator = $this->operator ?? "=";
return ["(".format_object_name($this->column)." $operator $valueSql $appendSql)", [...$bindings, ...$appendBindings]];
}
}
else
{ // No column defined, just return subconditions, if there are some.
if (str_starts_with($appendSql, "OR")) $appendSql = trim(substr($appendSql, 2));
if (str_starts_with($appendSql, "AND")) $appendSql = trim(substr($appendSql, 3));
return ["($appendSql)", $appendBindings];
}
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace Nest\Database\Query\Where;
use Nest\Database\Exceptions\Query\NoPrimaryFieldException;
use Nest\Model\Entities;
use Nest\Model\Entity;
/**
* Add where capacity to a query builder.
*/
trait HasWhere
{
/**
* Conditions in where clause.
* @var ConditionBuilder[]
*/
protected array $wheres = [];
/**
* Reset WHERE conditions.
* @return $this
*/
public function resetWheres(): static
{
$this->wheres = [];
return $this;
}
/**
* Add a where condition.
* @param string|callable $column The column of the condition, or a condition builder callable.
* @param mixed|null $operator The operator of the condition (or the value if no value is passed).
* @param mixed|null $value The value of the condition.
* @return $this
*/
public function where(string|callable $column, mixed $operator = null, mixed $value = null): static
{
if (is_callable($column))
{ // Callable condition builder.
$this->wheres[] = $column(new ConditionBuilder());
}
else
{ // Simple condition, registering it.
if (!empty($operator) && empty($value))
{ // If there are 2 parameters, considering the second one as the value, with default operator.
$value = $operator;
$operator = null;
}
if (!empty($value))
{ // A value is defined.
// Default operator: "=".
if (empty($operator)) $operator = "=";
// Create the simple condition.
$this->wheres[] = (new ConditionBuilder())->column($column)->operator($operator)->value($value);
}
else
{ // No value is given, considering a simple existence check.
$this->whereExists($column);
}
}
return $this;
}
/**
* Add a where IN condition.
* @param string $column The column of the condition.
* @param array $values The values of the condition.
* @return $this
*/
public function whereIn(string $column, array $values): static
{
return $this->where($column, "IN", $values);
}
/**
* Add a where condition on a column as a value.
* @param string $column The column of the condition.
* @param string $operator The operator of the condition (or the value column if no value is passed).
* @param string|null $valueColumn The value column of the condition.
* @return $this
*/
public function whereColumn(string $column, string $operator, ?string $valueColumn = null): static
{
if (!empty($operator) && empty($valueColumn))
{ // If there are 2 parameters, considering the second one as the value, with default operator.
$valueColumn = $operator;
$operator = "=";
}
// Create the simple condition.
$this->wheres[] = (new ConditionBuilder())->column($column)->operator($operator)->column($valueColumn);
return $this;
}
/**
* Add a where exists condition.
* @param string $column The column to check for existence.
* @return $this
*/
public function whereExists(string $column): static
{
// Create an existence check.
$this->wheres[] = (new ConditionBuilder())->column($column)->exists();
return $this;
}
/**
* Add condition to find given entities.
* @param Entity|Entities $entities Entity or entities to find.
* @return $this
* @throws NoPrimaryFieldException
*/
public function whereKeyOf(Entity|Entities $entities): static
{
// Normalize parameter to a simple entities array.
if ($entities instanceof Entities)
$entitiesArray = $entities->get();
else
$entitiesArray = [$entities];
// Initialize entities conditions.
$entitiesConditions = [];
foreach ($entitiesArray as $entity)
{ // Add condition for each entity to find.
// Get current entity primary fields and normalize it to an array.
$primaryFields = $entity->getPrimaryFields();
if (!is_array($primaryFields)) $primaryFields = [$primaryFields];
if (empty($primaryFields))
// If there are no primary fields, thrown an exception to ensure not to do something on EVERY rows.
throw new NoPrimaryFieldException($entity);
// Create conditions for current entity.
$entityConditions = [];
foreach ($primaryFields as $primaryField)
// Create a condition builder for each field.
$entityConditions[] = fn (ConditionBuilder $condition) =>
$condition->column($primaryField)->equals()->value($entity->$primaryField);
// Add the full entity condition to the entities conditions array.
$entitiesConditions[] = fn (ConditionBuilder $condition) => $condition->and(...$entityConditions);
}
// Add condition to match every entities.
return $this->where(fn (ConditionBuilder $condition) => (
$condition->or(...$entitiesConditions)
));
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace Nest\Database\Transactions;
use Nest\Database\Database;
use Nest\Database\Exceptions\NotCurrentTransactionException;
use Symfony\Component\Uid\Uuid;
/**
* Instance of a database transaction.
*/
class Transaction
{
/**
* UUID of the transaction.
* @var string
*/
private string $uuid;
/**
* Determine if the transaction is active (started and not committed nor rolled back).
* @var bool
*/
private bool $active = false;
/**
* Subtransaction of the current transaction.
* @var Transaction|null
*/
protected ?Transaction $childTransaction = null;
/**
* Create a new transaction for the given database.
* @param Database $database The database.
* @param Transaction|null $parentTransaction The parent transaction, if there is one.
*/
public function __construct(protected Database $database, protected ?Transaction $parentTransaction = null)
{
$this->uuid = Uuid::v4()->toString();
}
/**
* Get UUID of the transaction.
* @return string
*/
public function getUuid(): string
{
return $this->uuid;
}
/**
* Get the parent transaction.
* @return Transaction|null
*/
public function getParent(): ?Transaction
{
return $this->parentTransaction;
}
/**
* Set the current child transaction of this transaction.
* @param Transaction|null $transaction The child transaction. NULL if there is no child transaction.
* @return void
*/
public function setChild(?Transaction $transaction): void
{
$this->childTransaction = $transaction;
}
/**
* Start the transaction.
* @return void
* @throws NotCurrentTransactionException
*/
public function start(): void
{
// Transaction activation state changed.
$this->active = true;
$this->database->onTransactionStateChanged($this);
// Start the transaction.
$this->database->getQueriesAdapter()->newTransaction();
}
/**
* Commit the transaction.
* @return void
* @throws NotCurrentTransactionException
*/
public function commit(): void
{
if (!$this->isActive())
// Do not commit a non-active transaction.
return;
if (!empty($this->childTransaction))
// First commit the child transaction, if there is one.
$this->childTransaction->commit();
// Transaction activation state changed.
$this->active = false;
$this->database->onTransactionStateChanged($this);
// Commit the current transaction.
$this->database->getQueriesAdapter()->commitTransaction();
}
/**
* Rollback the transaction.
* @return void
* @throws NotCurrentTransactionException
*/
public function rollback(): void
{
if (!$this->isActive())
// Do not rollback a non-active transaction.
return;
if (!empty($this->childTransaction))
// First rollback the child transaction, if there is one.
$this->childTransaction->rollback();
// Transaction activation state changed.
$this->active = false;
$this->database->onTransactionStateChanged($this);
// Rollback the current transaction.
$this->database->getQueriesAdapter()->rollbackTransaction();
}
/**
* Determine if the transaction is still active (not committed nor rolled back).
* @return bool
*/
public function isActive(): bool
{
return $this->active;
}
}