commit 4f1ac0c9b4184021ad29e80d0657576bdaa77d0a Author: Madeorsk Date: Fri Nov 8 17:12:46 2024 +0100 Initialize Nest model library. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dfd9ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# IDEA +.idea/ +*.iml + +# Composer +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e1812c4 --- /dev/null +++ b/composer.json @@ -0,0 +1,47 @@ +{ + "version": "dev-main", + "name": "nest/model", + "description": "Nest model service.", + "type": "library", + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "autoload": { + "psr-4": { + "Nest\\Model\\": "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/Database" + }, + { + "type": "vcs", + "url": "https://code.zeptotech.net/Nest/Cli" + } + ], + "minimum-stability": "dev", + "require": { + "php": "^8.3", + "nest/core": "dev-main", + "nest/events": "dev-main", + "nest/types": "dev-main", + "nest/database": "dev-main" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..13dc327 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1037 @@ +{ + "_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": "6182d56b596c65b42db9bd84e00d01bb", + "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": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "5bf2cfa94ac29c5b73cac3f9e7465c20c35852d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/5bf2cfa94ac29c5b73cac3f9e7465c20c35852d5", + "reference": "5bf2cfa94ac29c5b73cac3f9e7465c20c35852d5", + "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" + }, + "default-branch": true, + "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-07T23:00:05+00:00" + }, + { + "name": "nest/cli", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://code.zeptotech.net/Nest/Cli", + "reference": "0361a0ef0dc03722caa8628bf7496864154e1b2a" + }, + "require": { + "nest/core": "dev-main", + "php": "^8.3" + }, + "default-branch": true, + "type": "library", + "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/database", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://code.zeptotech.net/Nest/Database", + "reference": "14836973b7a5ffd7ab86377e094a61ffe16962a6" + }, + "require": { + "ext-pdo": "*", + "nesbot/carbon": "^3.0", + "nest/cli": "dev-main", + "nest/core": "dev-main", + "nest/events": "dev-main", + "nest/model": "dev-main", + "nest/types": "dev-main", + "php": "^8.3", + "symfony/uid": "^7.1" + }, + "suggest": { + "nest/configuration": "dev-main" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Nest\\Database\\": "src/" + }, + "files": [ + "src/Utils/Sql.php" + ] + }, + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "description": "Nest databases service.", + "time": "2024-11-08T15:33:44+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": "7.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "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/7.2" + }, + "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:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.6-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/main" + }, + "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:21:43+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "2369cb908b33d7b7518cce042615de430142497f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2369cb908b33d7b7518cce042615de430142497f", + "reference": "2369cb908b33d7b7518cce042615de430142497f", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "default-branch": true, + "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/1.x" + }, + "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-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "1.x-dev", + "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" + }, + "default-branch": true, + "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": "1.x-dev", + "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" + }, + "default-branch": true, + "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": "7.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "f1faf9a381d39d0bf8ca1c10cab7dcf41fba50dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/f1faf9a381d39d0bf8ca1c10cab7dcf41fba50dc", + "reference": "f1faf9a381d39d0bf8ca1c10cab7dcf41fba50dc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "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/7.2" + }, + "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-10-23T06:56:12+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.6-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/main" + }, + "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-27T08:32:26+00:00" + }, + { + "name": "symfony/uid", + "version": "7.2.x-dev", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", + "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "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/7.2" + }, + "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:21:43+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "nest/core": 20, + "nest/database": 20, + "nest/events": 20, + "nest/types": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/src/ArrayBlueprint.php b/src/ArrayBlueprint.php new file mode 100644 index 0000000..e786ae4 --- /dev/null +++ b/src/ArrayBlueprint.php @@ -0,0 +1,46 @@ +type = $type; + return $this; + } + + public function table(string $table): static + { + $this->table = $table; + return $this; + } + + public function foreignKeyName(string $foreignKeyName): static + { + $this->foreignValueName = $foreignKeyName; + return $this; + } + + public function foreignValueName(string $foreignValueName): static + { + $this->foreignValueName = $foreignValueName; + return $this; + } +} diff --git a/src/ArrayInstance.php b/src/ArrayInstance.php new file mode 100644 index 0000000..71c9f88 --- /dev/null +++ b/src/ArrayInstance.php @@ -0,0 +1,120 @@ +entityClass, $array->name); + + // Copy all fields. + $this->type = $array->type; + $this->table = $array->table; + $this->foreignKeyName = $array->foreignKeyName; + $this->foreignValueName = $array->foreignValueName; + } + + /** + * Get current table / foreign key / foreign value state. + * @param string $fromTable Table from which to get values. + * @return array{string, string, string} Table, Foreign key, Foreign value. + */ + public function getState(string $fromTable): array + { + return [ + $table = $this->getTable() ?? "{$fromTable}_{$this->name}", + $foreignKey = $this->getForeignKeyName() ?? "$table.".str_snake_singularize($fromTable)."_id", + $foreignValue = $this->getForeignValueName() ?? "$table.".str_snake_singularize($this->name), + ]; + } + + /** + * Extract a column name from a fully qualified name. + * @return string The column name. + */ + private function getColumnName(string $fullyQualifiedName): string + { + return substr($fullyQualifiedName, strrpos($fullyQualifiedName, ".") + 1); + } + + /** + * Setup inline loading for the current array field in an entity query. + * @param EntityQuery $baseQuery The entity query to alter. + * @param string $prefix Prefix to add to the name of the properties. + * @return Raw Generated select subquery. + * @throws MissingConditionValueException + */ + public function genSelect(EntityQuery $baseQuery, string $prefix = ""): Raw + { + // Get all auto values for undefined tables or keys. + [$table, $foreignKey, $foreignValue] = $this->getState($baseQuery->getTableName()); + + // Build subquery SELECT SQL. + $sql = ((new SelectQuery($baseQuery->getDatabase(), $table)) + ->select($foreignValue) + ->whereColumn("{$baseQuery->getTableName()}.".$baseQuery->getPrimaryKeyName(), "=", $foreignKey))->build(); + + // Return the raw SELECT of a JSON array. + return new Raw( + "ARRAY_TO_JSON(ARRAY($sql->sql)) AS $prefix$this->name", + $sql->bindings, + ); + } + + /** + * @param Entity $entity Entity for which to save. + * @param array $value Array to save. + * @return void + * @throws MissingConditionValueException + * @throws UnknownDatabaseException + * @throws NotCurrentTransactionException + */ + public function save(Entity $entity, array $value): void + { + // Start a transaction for the whole save. + $transaction = $entity->getDatabase()->startTransaction(); + + // Get all auto values for undefined tables or keys. + [$table, $foreignKey, $foreignValue] = $this->getState($entity->getTableName()); + + // Get entity primary key value. + $entityKey = $entity->{array_first_or_val($entity->getPrimaryFields())}; + + // Delete existing values. + ((new DeleteQuery($entity->getDatabase(), $table)) + ->where($foreignKey, "=", $entityKey)) + ->execute(); + + // Get actual columns names. + $foreignKey = $this->getColumnName($foreignKey); + $foreignValue = $this->getColumnName($foreignValue); + + // Insert new ones. + ((new InsertQuery($entity->getDatabase(), $table))) + // Insert all values of the array. + ->values(...array_map(fn (mixed $val) => ([ + $foreignKey => $entityKey, + $foreignValue => $val, + ]), $value)) + ->execute(); + + // Commit transaction. + $transaction->commit(); + } +} diff --git a/src/Entities.php b/src/Entities.php new file mode 100644 index 0000000..487a8d0 --- /dev/null +++ b/src/Entities.php @@ -0,0 +1,125 @@ +entities = $entities; + } + + /** + * Get entities array. + * @return Entity[] The entities. + */ + public function get(): array + { + return $this->entities; + } + + /** + * Set values to all the entities. + * @param string $name Name of the value to set. + * @param mixed $value Value to set. + * @return void + */ + public function __set(string $name, mixed $value): void + { + foreach ($this->entities as $entity) + $entity->$name = $value; + } + + /** + * Get all values with the given name in the entities. + * @param string $name Name of the value to get. + * @return array Array of values. + */ + public function __get(string $name): array + { + $result = []; + foreach ($this->entities as $entity) + $result[] = $entity->$name; + return $result; + } + + /** + * Call the given method on all entities. + * @param string $name Name of the method to call. + * @param array $arguments Arguments of the method. + * @return array Results of all calls. + */ + public function __call(string $name, array $arguments): array + { + $results = []; + foreach ($this->entities as $entity) + $results[] = $entity->$name(...$arguments); + return $results; + } + + /** + * Save current entities states. + * @return bool True if something was saved, false otherwise. + * @throws IncompatibleTypeException + * @throws MissingConditionValueException + * @throws MissingRequiredFieldException + * @throws NoPrimaryFieldException + * @throws InvalidTypeException + */ + public function save(): bool + { + $result = false; + foreach ($this->entities as $entity) + // Set result to true if needed. + $result = $result || $entity->save(); + return $result; + } + + /** + * Load given relation in the entities. + * @param array $relations Relations to load. + * @return $this + * @throws UnknownDatabaseException + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + */ + public function load(array $relations): static + { + // Do nothing if there are no entities. + if (empty($this->entities)) return $this; + + // Normalize relations array. + $relations = EntityQuery::normalizeRelationsDefinition($relations); + + // Perform load for all the entities. + array_first($this->entities)->query()->load($this->entities, $relations); + + return $this; + } +} diff --git a/src/Entity.php b/src/Entity.php new file mode 100644 index 0000000..7e501c6 --- /dev/null +++ b/src/Entity.php @@ -0,0 +1,586 @@ +_definition)) + { // Get a new definition and save it in cached definition. + $this->_definition = $this->definition(new EntityBlueprint(static::class)); + $this->getEventsManager()->fire(new EntityDefinitionEvent($this)); + } + + // Return cached definition. + return $this->_definition; + } + + /** + * Initialize entity. + */ + public function __construct() + { + // Initialize properties. + $this->initializeProperties(); + } + + /** + * Initialize entity default properties. + * @return void + */ + protected function initializeProperties(): void + { + $this->getEventsManager()->fire(new BeforePropertiesInitialization($this)); + + foreach ($this->_getDefinition()->getProperties() as $propertyName => $property) + { // For each property, set its default value if there is one. + if ($property instanceof ReadableFieldBlueprint && $property->hasCurrentDateByDefault()) + $this->$propertyName = Carbon::now(); + } + + $this->getEventsManager()->fire(new AfterPropertiesInitialization($this)); + } + + + /** + * Guess the entity database table name from its class name. + * Always use a plural snake_cased name. + * @return string The database table name of the entity, based on its class name. + */ + private function guessTableName(): string + { + return str_snake_pluralize(str_camel_to_snake(get_class_name_from_fullname( + (new \ReflectionClass($this))->isAnonymous() ? get_parent_class(static::class) : static::class, + ))); + } + + /** + * Get the entity database table name. + * @return string The database table name of the entity. + */ + public function getTableName(): string + { + return $this->_getDefinition()->getTable() ?? $this->guessTableName(); + } + + /** + * Check that the required fields are set. + * @return string[] Names of the missing required fields. + */ + public function getMissingRequiredFields(): array + { + // Get current entity definition. + $entityBlueprint = $this->_getDefinition(); + // Get its required fields. + $requiredFields = array_filter($entityBlueprint->getProperties(), fn ($field) => $field instanceof ReadableFieldBlueprint && $field->isRequired()); + + // Initialize missing required fields list. + $missingRequiredFields = []; + + foreach ($requiredFields as $requiredFieldName => $requiredField) + { // If any required field is not set, add it in the list. + if (!isset($this->{$requiredFieldName})) + // A required field is not set. + $missingRequiredFields[] = $requiredFieldName; + } + + return $missingRequiredFields; // All the missing required fields. + } + + /** + * Check that all the required fields are set. + * Throw an exception if some are not set. + * @return void + * @throws MissingRequiredFieldException + */ + public function checkRequiredFields(): void + { + if (!empty($missingFields = $this->getMissingRequiredFields())) + // Some required fields are missing, throwing an exception. + throw new MissingRequiredFieldException($missingFields); + } + + /** + * Get primary fields. + * @return string|string[] The primary field name, or array of fields when there are multiple primary fields. + */ + public function getPrimaryFields(): string|array + { + // Search primary fields. + $primaryFields = array_filter($this->_getDefinition()->getProperties(), fn ($field) => $field instanceof ReadableFieldBlueprint && $field->isPrimary()); + + // Return primary field(s). + return count($primaryFields) == 1 ? array_key_first($primaryFields) : array_keys($primaryFields); + } + + + /** + * Get database of the entity. + * @return Database Database of the entity. + * @throws UnknownDatabaseException + */ + public function getDatabase(): Database + { + return Application::get()->databases()->db($this->_getDefinition()->getDatabase()); + } + + /** + * Create a new SQL query for the entity. + * @return QueryBuilder SQL query builder. + * @throws UnknownDatabaseException + */ + public function sql(): QueryBuilder + { + return $this->getDatabase()->query($this->getTableName()); + } + + /** + * Create a new entity query. + * @return EntityQuery Entity query builder. + * @throws UnknownDatabaseException + */ + public function query(): EntityQuery + { + // Create the query. + $query = new EntityQuery($this->getDatabase(), $this->getTableName(), $this); + + // Automatically fill "with" with eager loaded properties. + $query->with(array_filter(array_map(fn ($property) => ( + $property instanceof ReadableEntityPropertyBlueprint && $property->doEagerLoad() + ), $this->_getDefinition()->getProperties()))); + + return $query; // Return the created query. + } + + /** + * Load given relations in the entity. + * @param array $relations Relations to load. + * @return $this + * @throws UnknownDatabaseException + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + * @throws UnhandledPropertyTypeException + */ + public function load(array $relations): static + { + // Normalize relations array. + $relations = EntityQuery::normalizeRelationsDefinition($relations); + + $this->getEventsManager()->fire(new BeforeLoad($this, $relations)); + + // Perform load for the current entity. + $this->query()->load([$this], $relations); + + $this->getEventsManager()->fire(new AfterLoad($this, $relations)); + + return $this; + } + + /** + * Create a new entity of the same type. + * @return static A new entity of the same type. + */ + public function new(): static + { + return new static(); + } + + /** + * Set an original property value. + * @param string $propertyName Property name. + * @param mixed $propertyValue Property value. + * @return void + */ + public function setOriginalProperty(string $propertyName, mixed $propertyValue): void + { + $this->$propertyName = $propertyValue; + $this->originals[$propertyName] = $propertyValue; + } + + /** + * Find changes made on the entity. + * @return array New properties values. + */ + public function diff(): array + { + // Initialize diff array. + $diff = []; + + foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition) + { // Add current property value to diff if it's different from original value. + if (!($propertyDefinition instanceof FieldBlueprint || $propertyDefinition instanceof ArrayBlueprint)) + // Only keep normal fields. + continue; + + if (isset($this->$propertyName) && (!isset($this->originals[$propertyName]) || $this->originals[$propertyName] != $this->$propertyName)) + // Current value is different from original one, adding it to the diff. + $diff[$propertyName] = $this->$propertyName; + } + + return $diff; // Return properties diff. + } + + /** + * Save the current entity state. + * @return bool True if something was saved, false otherwise. + * @throws IncompatibleTypeException + * @throws MissingConditionValueException + * @throws MissingRequiredFieldException + * @throws NoPrimaryFieldException + * @throws UnhandledPropertyTypeException + * @throws UnknownDatabaseException + */ + public function save(): bool + { + $this->getEventsManager()->fire(new BeforeSave($this)); + + // Check required fields to save. + $this->checkRequiredFields(); + + if ($this->isNew) + { // This is a new entity, inserting it, then reading the result. + $this->fromSqlProperties($this->insert()); + } + else + { // The entity already exists, updating it. + + // Get entity diff. + $diff = $this->diff(); + + // No diff, return false because there is nothing to update. + if (empty($diff)) return false; + + // Get properties definition. + $properties = $this->_getDefinition()->getProperties(); + + foreach ($diff as $propertyName => $propertyValue) + { // For each property in diff, check if it's an array field. + if ($properties[$propertyName] instanceof ArrayBlueprint) + { // If the property is an array field, save it then remove it from diff. + (new ArrayInstance($properties[$propertyName]))->save($this, $propertyValue); + unset($diff[$propertyName]); + } + } + + // Update every else property (if there are still some). + if (!empty($diff)) + $this->fromSqlProperties($this->update($diff)); + } + + $this->getEventsManager()->fire(new AfterSave($this)); + + return true; // Entity saved. + } + + /** + * Delete the entity. + * @return bool + * @throws MissingConditionValueException + * @throws NoPrimaryFieldException + * @throws UnknownDatabaseException + */ + public function delete(): bool + { + $this->getEventsManager()->fire(new BeforeDelete($this)); + + // Delete current entity. + $this->sql()->newDelete()->whereKeyOf($this)->execute(); + + // Set entity as new, in case of a save after deletion. + $this->isNew = true; + + $this->getEventsManager()->fire(new AfterDelete($this)); + + return true; // Entity is deleted. + } + + /** + * Insert current entity values. + * @return object Raw inserted object. + * @throws IncompatibleTypeException + * @throws UnknownDatabaseException + */ + private function insert(): object + { + $this->getEventsManager()->fire(new BeforeInsert($this)); + + // Return the first result (= the sole inserted row). + $result = array_first( + // Execute INSERT in RETURNING mode. + $this->sql()->newInsert()->values( + // Extract properties in an SQL row values array. + $this->toSqlProperties() + )->execute(true) + ); + + $this->getEventsManager()->fire(new AfterInsert($this)); + + return $result; + } + + /** + * Update given entity values. + * @param array $values Values to update. + * @return object Raw updated object. + * @throws MissingConditionValueException + * @throws IncompatibleTypeException + * @throws NoPrimaryFieldException + * @throws UnknownDatabaseException + */ + private function update(array $values): object + { + $this->getEventsManager()->fire(new BeforeUpdate($this)); + + // Return the first result (= the sole updated row). + $result = array_first( + // Execute UPDATE in RETURNING mode. + $this->sql()->newUpdate()->set($this->valuesToSql($values)) + ->whereKeyOf($this) + ->execute(true) + ); + + $this->getEventsManager()->fire(new AfterUpdate($this)); + + return $result; + } + + /** + * Get relation of the given name. + * @param string $relationName Name of the relation to get. + * @return EntityPropertyInstance The property instance. + * @throws UndefinedRelationException + */ + public function getRelation(string $relationName): EntityPropertyInstance + { + // Get defined properties for the entity. + $properties = $this->_getDefinition()->getProperties(); + + if (!empty($properties[$relationName])) + { // A property with the relation name exists, trying to get its relation definition. + $definition = $properties[$relationName]; + if (is_a($definition, EntityPropertyBlueprint::class)) + // Initialize a property instance. + return new EntityPropertyInstance($definition); + else + { // Not an entity blueprint, throw an exception. + throw new UndefinedRelationException(static::class, $relationName); + } + } + else + { // No property with this name, throw an exception. + throw new UndefinedRelationException(static::class, $relationName); + } + } + + /** + * Convert given values to SQL from matching properties types. + * @param array $values Values to convert to SQL value. + * @return array SQL values. + * @throws IncompatibleTypeException + */ + private function valuesToSql(array $values): array + { + // Get properties definition. + $propertiesDefinition = $this->_getDefinition()->getProperties(); + + foreach ($values as $propertyName => $value) + { // For each value, try to convert its data. + + // Keep the value (and convert it) if there is a matching property. + if (!empty($propertiesDefinition[$propertyName])) + { + // Get property type. + $propertyType = $propertiesDefinition[$propertyName]->getTypeInstance(); + + if (!$propertyType instanceof SqlType) + // Type is not an SQL type, throwing an exception. + throw new IncompatibleTypeException(get_class($propertyType), SqlType::class); + + // Converting property value and put it in the SQL object. + $values[$propertyName] = $propertyType->toSql($value); + } + else + // No property matching the current value, unset it. + unset($values[$propertyName]); + } + + return $values; // Return converted values. + } + + /** + * Import properties from a raw SQL object. + * @param object $sqlProperties Raw SQL object where to extract properties. + * @param string $prefix Optional prefix of properties in SQL. + * @return void + * @throws IncompatibleTypeException + * @throws UnhandledPropertyTypeException + */ + public function fromSqlProperties(object $sqlProperties, string $prefix = ""): void + { + $this->isNew = false; // The entity already exists, as it's read from SQL properties. + + foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition) + { // For each defined property, try to read its data. + // Prepend property name by the prefix. + $propertyName = $prefix.$propertyName; + + // Try to read only if a value is present. + if (isset($sqlProperties->$propertyName)) + { // Get property type. + if (!($propertyDefinition instanceof FieldBlueprint || $propertyDefinition instanceof ArrayBlueprint)) + throw new UnhandledPropertyTypeException($propertyName, get_class($propertyDefinition)); + + // Get property type instance. + $propertyType = $propertyDefinition->getTypeInstance(); + + if (!$propertyType instanceof SqlType) + // Type is not an SQL type, throwing an exception. + throw new IncompatibleTypeException(get_class($propertyType), SqlType::class); + + if ($propertyDefinition instanceof FieldBlueprint) + { // Read direct value. + // Parsing property value and put it in the entity. + $value = $propertyType->fromSql($sqlProperties->$propertyName); + } + else + { // Read JSON array value. + $value = json_decode($sqlProperties->$propertyName); + // Parsing array values. + foreach ($value as &$val) $val = $propertyType->fromSql($val); + } + + // Set original property value. + $this->setOriginalProperty($propertyName, $value); + } + } + } + + /** + * Export properties to a raw SQL object. + * @return array Raw SQL array where properties are extracted. + * @param string $prefix Optional prefix of properties in SQL. + * @throws IncompatibleTypeException + */ + public function toSqlProperties(string $prefix = ""): array + { + // Initialize raw SQL array. + $object = []; + + foreach ($this->_getDefinition()->getProperties() as $propertyName => $propertyDefinition) + { // Get SQL value of each defined property. + // Prepend property name by the prefix. + $propertyName = $prefix.$propertyName; + + if (isset($this->$propertyName)) + { // If property is set, converting it. + // Get property type. + $propertyType = $propertyDefinition->getTypeInstance(); + + if (!$propertyType instanceof SqlType) + // Type is not an SQL type, throwing an exception. + throw new IncompatibleTypeException(get_class($propertyType), SqlType::class); + + // Converting property value and put it in the SQL object. + $object[$propertyName] = $propertyType->toSql($this->$propertyName); + } + } + + return $object; // Return built raw SQL array. + } + + /** + * Generate columns SELECT for the defined properties. + * @param string $prefix Prefix to add to the name of the properties. + * @return Raw[] Selected columns. + * @throws UnknownDatabaseException + * @throws MissingConditionValueException + */ + public function sqlSelectFields(string $prefix = ""): array + { + $select = []; + + foreach ($this->_getDefinition()->getProperties() as $propertyName => $property) + { // For each field, generate a column selection. + if ($property instanceof FieldBlueprint) + { // It's a field property, generate a SELECT for it. + $select[] = new Raw("\"{$this->getTableName()}\".\"$propertyName\"".(!empty($prefix) ? " AS \"$prefix$propertyName\"" : "")); + } + elseif ($property instanceof ArrayBlueprint) + { // It's an array field, generate a SELECT for it. + $select[] = (new ArrayInstance($property))->genSelect($this->query(), $prefix); + } + } + + return $select; + } +} diff --git a/src/EntityBlueprint.php b/src/EntityBlueprint.php new file mode 100644 index 0000000..8a6fb46 --- /dev/null +++ b/src/EntityBlueprint.php @@ -0,0 +1,155 @@ + + */ + protected string $entityClass; + + /** + * Database identifier of the entity. + * @var string|null + */ + protected ?string $database = null; + + /** + * Database table name of the entity. + * @var string|null + */ + protected ?string $table = null; + + /** + * Properties definition array. + * @var array + */ + protected array $properties = []; + + /** + * @param class-string $entityClass Entity class. + */ + public function __construct(string $entityClass) + { + $this->entityClass = $entityClass; + } + + /** + * Set the database table name of the entity. + * @param string $table The database table name of the entity. + * @return $this + */ + public function setTable(string $table): static + { + $this->table = $table; + return $this; + } + + /** + * Get the database table name of the entity. + * @return string|null The database table name. + */ + public function getTable(): ?string + { + return $this->table; + } + + /** + * Set the database of the entity. + * @param string $databaseIdentifier The database identifier. + * @return $this + */ + public function setDatabase(string $databaseIdentifier): static + { + $this->database = $databaseIdentifier; + return $this; + } + + /** + * Get the database of the entity. + * @return string|null The database identifier. + */ + public function getDatabase(): ?string + { + return $this->database ?? "default"; + } + + /** + * Get the defined properties. + * @return array The defined properties. + */ + public function getProperties(): array + { + return $this->properties; + } + + public function field(string $name, string $type): FieldBlueprint + { + return $this->properties[$name] = (new ReadableFieldBlueprint())->type($type); + } + + public function id(string $name = "id", string $type = BigintType::class): FieldBlueprint + { + return $this->foreignId($name, $type)->autoIncrement()->primary(); + } + + public function foreignId(string $name, string $type = BigintType::class): FieldBlueprint + { + return $this->field($name, $type)->unsigned()->index(); + } + + public function createdAt(string $name = "created_at", string $type = CarbonType::class): FieldBlueprint + { + return $this->field($name, $type)->required()->currentDateByDefault(); + } + + public function updatedAt(string $name = "updated_at", string $type = CarbonType::class): FieldBlueprint + { + return $this->field($name, $type)->required()->currentDateByDefault()->currentDateOnUpdate(); + } + + + + /** + * Define an array field. + * @param string $name The name of the field. + * @param string $type The type of the array values. + * @return ArrayBlueprint The array blueprint. + */ + public function array(string $name, string $type): ArrayBlueprint + { + return $this->properties[$name] = (new ReadableArrayBlueprint($this->entityClass, $name))->type($type); + } + + + /** + * Define an entity property. + * @param string $name The name of the property. + * @param string $class The class of the property. + * @param bool $multiple Whether the property is an array or not. + * @return EntityPropertyBlueprint The property blueprint. + */ + public function entity(string $name, string $class, bool $multiple = false): EntityPropertyBlueprint + { + return $this->properties[$name] = (new ReadableEntityPropertyBlueprint())->class($class)->multiple($multiple); + } + + /** + * Define an entities array property. + * @param string $name The name of the property. + * @param string $class The class of the property. + * @return EntityPropertyBlueprint The property blueprint. + */ + public function entities(string $name, string $class): EntityPropertyBlueprint + { + return $this->entity($name, $class, true); + } +} diff --git a/src/EntityPropertyBlueprint.php b/src/EntityPropertyBlueprint.php new file mode 100644 index 0000000..5328012 --- /dev/null +++ b/src/EntityPropertyBlueprint.php @@ -0,0 +1,106 @@ +class = $class; + return $this; + } + + public function multiple(bool $multiple = true): static + { + $this->multiple = $multiple; + return $this; + } + + /** + * Set if inline loading of the relation is allowed or not. + * @param bool $inline True to allow relation inline loading. + * @return $this + */ + public function allowInline(bool $inline = true): static + { + $this->allowInline = $inline; + return $this; + } + + public function fromLocal(?string $localKey = null, ?string $relatedKey = null): static + { + $this->mode = EntityPropertyMode::LOCAL; + $this->localKey = $localKey; + $this->relatedKey = $relatedKey; + return $this; + } + public function fromRelated(?string $relatedKey = null, ?string $localKey = null): static + { + $this->mode = EntityPropertyMode::RELATED; + $this->localKey = $localKey; + $this->relatedKey = $relatedKey; + return $this; + } + + public function localKey(string $localKey): static + { + $this->localKey = $localKey; + return $this; + } + public function relatedKey(string $relatedKey): static + { + $this->relatedKey = $relatedKey; + return $this; + } + + public function fromPivot(string $pivotTable, ?string $pivotLocalKey = null, ?string $pivotRelatedKey = null): static + { + $this->mode = EntityPropertyMode::PIVOT; + $this->pivotLocalKey = $pivotLocalKey; + $this->pivotRelatedKey = $pivotRelatedKey; + return $this; + } + + public function pivotLocalKey(string $pivotLocalKey): static + { + $this->pivotLocalKey = $pivotLocalKey; + return $this; + } + public function pivotRelatedKey(string $pivotRelatedKey): static + { + $this->pivotRelatedKey = $pivotRelatedKey; + return $this; + } + + /** + * Set if the related field should be eager loaded or not. + * Only apply on related fields. + * @param bool $load True to eager load the field. + * @return $this + */ + public function eagerLoad(bool $load = true): static + { + $this->eagerLoad = $load; + return $this; + } +} diff --git a/src/EntityPropertyInstance.php b/src/EntityPropertyInstance.php new file mode 100644 index 0000000..909a420 --- /dev/null +++ b/src/EntityPropertyInstance.php @@ -0,0 +1,200 @@ +class = $entityProperty->class; + $this->multiple = $entityProperty->multiple; + $this->allowInline = $entityProperty->allowInline; + $this->mode = $entityProperty->mode; + $this->localKey = $entityProperty->localKey; + $this->relatedKey = $entityProperty->relatedKey; + $this->pivotTable = $entityProperty->pivotTable; + $this->pivotLocalKey = $entityProperty->pivotLocalKey; + $this->pivotRelatedKey = $entityProperty->pivotRelatedKey; + $this->eagerLoad = $entityProperty->eagerLoad; + + // Initialize goal entity. + $this->entity = new ($this->getClass())(); + } + + /** + * Create a new related entity. + * @return Entity A new related entity instance. + */ + public function newEntity(): Entity + { + return $this->entity->new(); + } + + /** + * Determine if the property can be inline loaded. + * @return bool True if the property can be inline loaded. + */ + public function canInlineLoad(): bool + { + return !$this->isMultiple() && $this->isInlineAllowed(); + } + + /** + * Build a query to retrieve relations for the given entities. + * @param Entity[] $entities Entities for which to retrieve relations. + * @return EntityQuery Built entities query. + * @throws UnknownDatabaseException + */ + public function queryFor(array $entities): EntityQuery + { + // Initialize query. + $query = $this->entity->query(); + + if (empty($entities)) + // No entities, return a simple query without conditions. + return $query; + + // Get a reference entity. + $referenceEntity = array_first($entities); + // Entity primary key. + $entityPrimaryKey = array_first_or_val($referenceEntity->getPrimaryFields()); + + // Get keys from entities list. + $entitiesKeys = array_column($entities, $entityPrimaryKey); + + switch ($this->getMode()) + { // Build query depending on the relation mode. + case EntityPropertyMode::LOCAL: + $query + ->select(new Raw("\"{$referenceEntity->getTableName()}\".\"$entityPrimaryKey\" AS \"__reference_key\"")) + // Generate SELECT for the related entity. + ->select(...$this->entity->sqlSelectFields()) + ->innerJoin($referenceEntity->getTableName())->on( + "{$this->entity->getTableName()}.".( + $this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields()) + ), + "=", + "{$referenceEntity->getTableName()}.".( + $this->getLocalKey() ?? str_snake_singularize($this->entity->getTableName())."_id" + ), + )->whereIn( + "{$referenceEntity->getTableName()}.$entityPrimaryKey", $entitiesKeys + ); + break; + case EntityPropertyMode::RELATED: + $query + ->select(new Raw("\"{$this->entity->getTableName()}\".\"".( + $this->getRelatedKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id" + )."\" AS \"__reference_key\"")) + // Generate SELECT for the related entity. + ->select(...$this->entity->sqlSelectFields()) + ->whereIn( + "{$this->entity->getTableName()}.".( + $this->getRelatedKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id" + )."", + $entitiesKeys + ); + break; + case EntityPropertyMode::PIVOT: + $query + ->select(new Raw("\"{$this->entity->getTableName()}\".\"".( + $this->getPivotLocalKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id" + )."\" AS \"__reference_key\"")) + // Generate SELECT for the related entity. + ->select(...$this->entity->sqlSelectFields()) + ->innerJoin($this->getPivotTable())->on( + "{$query->getTableName()}.".( + $this->getRelatedKey() ?? $query->getPrimaryKeyName() + ), + "=", + "{$this->getPivotTable()}.".( + $this->getPivotLocalKey() ?? str_snake_singularize($query->getTableName())."_id" + ), + ) + ->whereIn( + "{$this->getPivotTable()}.".( + $this->getPivotLocalKey() ?? str_snake_singularize($referenceEntity->getTableName())."_id" + ), + $entitiesKeys + ); + break; + } + + return $query; + } + + /** + * Setup inline loading for the current property in an entity query. + * @param EntityQuery $query The entity query to alter. + * @return void + */ + public function setupInlineLoading(EntityQuery $query): void + { + switch ($this->getMode()) + { // Add join depending on the relation mode. + case EntityPropertyMode::LOCAL: + $query + ->leftJoin($this->entity->getTableName())->on( + "{$query->getTableName()}.".( + $this->getLocalKey() ?? str_snake_singularize($this->entity->getTableName())."_id" + ), + "=", + "{$this->entity->getTableName()}.".( + $this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields()) + ), + ); + break; + case EntityPropertyMode::RELATED: + $query + ->leftJoin($this->entity->getTableName())->on( + "{$query->getTableName()}.".( + $this->getLocalKey() ?? $query->getPrimaryKeyName() + ), + "=", + "{$this->entity->getTableName()}.".( + $this->getRelatedKey() ?? str_snake_singularize($query->getTableName())."_id" + ), + ); + break; + case EntityPropertyMode::PIVOT: + $query + ->leftJoin($this->getPivotTable())->on( + "{$query->getTableName()}.".( + $this->getLocalKey() ?? $query->getPrimaryKeyName() + ), + "=", + "{$this->getPivotTable()}.".( + $this->getPivotLocalKey() ?? str_snake_singularize($query->getTableName())."_id" + ), + ) + ->leftJoin($this->entity->getTableName())->on( + "{$this->getPivotTable()}.".( + $this->getPivotRelatedKey() ?? str_snake_singularize($this->entity->getTableName())."_id" + ), + "=", + "{$this->entity->getTableName()}.".( + $this->getRelatedKey() ?? array_first_or_val($this->entity->getPrimaryFields()) + ), + ); + break; + } + } +} diff --git a/src/EntityPropertyMode.php b/src/EntityPropertyMode.php new file mode 100644 index 0000000..113e6ad --- /dev/null +++ b/src/EntityPropertyMode.php @@ -0,0 +1,10 @@ + $relations Loaded relations. + */ + public function __construct(Entity $entity, public readonly array $relations) + { + parent::__construct($entity); + } +} diff --git a/src/Events/AfterPropertiesInitialization.php b/src/Events/AfterPropertiesInitialization.php new file mode 100644 index 0000000..609abbf --- /dev/null +++ b/src/Events/AfterPropertiesInitialization.php @@ -0,0 +1,10 @@ + $relations Relations to load. + */ + public function __construct(Entity $entity, public readonly array $relations) + { + parent::__construct($entity); + } +} diff --git a/src/Events/BeforePropertiesInitialization.php b/src/Events/BeforePropertiesInitialization.php new file mode 100644 index 0000000..bffefda --- /dev/null +++ b/src/Events/BeforePropertiesInitialization.php @@ -0,0 +1,10 @@ +missingFields), $code, $previous); + } +} diff --git a/src/Exceptions/ModelException.php b/src/Exceptions/ModelException.php new file mode 100644 index 0000000..343d774 --- /dev/null +++ b/src/Exceptions/ModelException.php @@ -0,0 +1,9 @@ +relationName in $this->modelClass.", $code, $previous); + } +} diff --git a/src/Exceptions/UnhandledPropertyTypeException.php b/src/Exceptions/UnhandledPropertyTypeException.php new file mode 100644 index 0000000..cacae5c --- /dev/null +++ b/src/Exceptions/UnhandledPropertyTypeException.php @@ -0,0 +1,22 @@ +propertyName of type $this->propertyType.", $code, $previous); + } +} diff --git a/src/FieldBlueprint.php b/src/FieldBlueprint.php new file mode 100644 index 0000000..0061a09 --- /dev/null +++ b/src/FieldBlueprint.php @@ -0,0 +1,77 @@ +type = $type; + return $this; + } + + public function primary(bool $primary = true): static + { + $this->primary = $primary; + return $this; + } + + public function index(bool $index = true): static + { + $this->index = $index; + return $this; + } + + public function unique(bool $unique = true): static + { + $this->unique = $unique; + return $this; + } + + public function required(bool $required = true): static + { + $this->required = $required; + return $this; + } + + public function autoIncrement(bool $autoIncrement = true): static + { + $this->autoIncrement = $autoIncrement; + return $this; + } + + public function unsigned(bool $unsigned = true): static + { + $this->unsigned = $unsigned; + return $this; + } + + public function currentDateByDefault(bool $currentDateByDefault = true): static + { + $this->currentDateByDefault = $currentDateByDefault; + return $this; + } + + public function currentDateOnUpdate(bool $currentDateOnUpdate = true): static + { + $this->currentDateOnUpdate = $currentDateOnUpdate; + return $this; + } +} diff --git a/src/Query/EntityQuery.php b/src/Query/EntityQuery.php new file mode 100644 index 0000000..5f585ee --- /dev/null +++ b/src/Query/EntityQuery.php @@ -0,0 +1,344 @@ + $relations Relations to normalize. + * @return array Relations definition. + */ + public static function normalizeRelationsDefinition(array $relations): array + { + // Normalize relations array. + foreach ($relations as $key => $relation) + { // Add each relation to eager load to the with array. + if (!is_string($key)) + { // If key is an integer, then $relation is a string key. + $relations[$relation] = true; + unset($relations[$key]); + } + } + + // Return normalized relations array. + return $relations; + } + + /** + * Relations that should be eager loaded. + * @var array + */ + protected array $with = []; + + /** + * @inheritDoc + * @param Entity $entity Reference entity. + */ + public function __construct(Database $database, string $table, protected Entity $entity) + { + parent::__construct($database, $table); + } + + /** + * Create a new entity instance. + * @return Entity Entity instance. + */ + protected function newEntity(): Entity + { + // Create a new entity instance. + return $this->entity->new(); + } + + /** + * Get queried entity primary key name. + * @return string Primary key name. + */ + public function getPrimaryKeyName(): string + { + return array_first_or_val($this->entity->getPrimaryFields()); + } + + /** + * Add a where condition for the primary key of the entity. + * @param mixed|array $keyValue Entity primary key value(s) to match. + * @return $this + */ + public function whereKey(mixed $keyValue): static + { + return $this->whereIn($this->getPrimaryKeyName(), is_array($keyValue) ? $keyValue : [$keyValue]); + } + + /** + * Make a relation prefix for the given relation. + * @param string $relationKey Relation key. + * @return string Relation prefix. + */ + private function getRelationPrefix(string $relationKey): string + { + return "{$this->entity->getTableName()}_{$relationKey}_"; + } + + /** + * Go through all first-level relations to eager load and add joins and selected columns for them. + * @param bool $generateSelect Set if SELECTed columns must be generated or not. + * @return string[] Loaded inline relations. + * @throws UndefinedRelationException + */ + private function addInlineRelations(bool $generateSelect): array + { + if (empty($this->with)) return []; + + // Initialize list of inline loaded relations. + $inlineRelations = []; + + foreach ($this->with as $relationKey => $relationCallable) + { // For each relation, if it's inline, append a JOIN and SELECT for it. + // Get the required relation. + $relation = $this->entity->getRelation($relationKey); + if ($relation->canInlineLoad()) + { // If the relation is not multiple, it can be retrieved inline. + $relation->setupInlineLoading($this); + + if ($generateSelect) + // Generate SELECT for the relation. + $this->select(...$relation->newEntity()->sqlSelectFields($this->getRelationPrefix($relationKey))); + + if (is_callable($relationCallable)) + // Call the callable with the alterable query. + $relationCallable($this); + + // Add to inline loaded relations. + $inlineRelations[] = $relationKey; + } + } + + return $inlineRelations; + } + + /** + * Get queried entities. + * @param string|null $indexProperty A property to use as entities array index. + * @return Entity[]|array Retrieved entities. + * @throws MissingConditionValueException + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws UndefinedRelationException + * @throws UnknownDatabaseException + * @throws UnhandledPropertyTypeException + */ + public function get(?string $indexProperty = null): array + { + // If there's no custom SELECT, generate it. + $generateSelect = empty($this->selected); + + if ($generateSelect) + // Generate SELECT for the main entity. + $this->select(...$this->entity->sqlSelectFields()); + + // Add inline relations retrieval to the query. + $inlineRelations = $this->addInlineRelations($generateSelect); + + // Map entities with their inline relations, if there are some. + $entities = $this->mapEntities($this->execute(), $inlineRelations, $indexProperty); + + // Load all remaining relations (remove already loaded inline relations). + $relationsToLoad = $this->with ?? []; + foreach ($inlineRelations as $inlineRelation) + unset($relationsToLoad[$inlineRelation]); + + $this->load($entities, $relationsToLoad); + + return $entities; + } + + /** + * Retrieve the entities with the given keys. + * @param mixed[] $keysValues Entity primary keys values to match. + * @return Entity[] Retrieved entities. + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + * @throws UnhandledPropertyTypeException + * @throws UnknownDatabaseException + */ + public function findMany(array $keysValues): array + { + return $this->whereKey($keysValues)->get(); + } + + /** + * Retrieve the first entity of the query. + * @return Entity Retrieved entity. + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + * @throws UnhandledPropertyTypeException + * @throws UnknownDatabaseException + */ + public function first(): Entity + { + return array_first($this->limit(1)->get()); + } + + /** + * Retrieve the entity with the given key. + * @param mixed $keyValue Primary key value of the entity. + * @return Entity Retrieved entity. + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + * @throws UnhandledPropertyTypeException + * @throws UnknownDatabaseException + */ + public function find(mixed $keyValue): Entity + { + return $this->whereKey($keyValue)->first(); + } + + /** + * Map raw objects results to entities. + * @param object[] $objects Raw objects results. + * @param string[] $inlineRelations Inline relations to map, if there are some. + * @return Entity[] The entities. + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws UndefinedRelationException + * @throws UnhandledPropertyTypeException + */ + protected function mapEntities(array $objects, array $inlineRelations = [], ?string $indexProperty = null): array + { + // Initialize entities array. + $entities = []; + + foreach ($objects as $object) + { // Map each object to an entity. + $entity = $this->mapEntity($object); + + // Append to entities array, depending on index property name. + if (empty($indexProperty)) + $entities[] = $entity; + else + { + if (empty($object->$indexProperty)) $object->$indexProperty = []; + $entities[$object->$indexProperty][] = $entity; + } + + // Map inline relations. + foreach ($inlineRelations as $inlineRelation) + { // For each inline relation, map its data. + $relation = $this->entity->getRelation($inlineRelation); + // Create a new entity as property value. + $propertyValue = $relation->newEntity(); + $propertyValue->fromSqlProperties($object, $this->getRelationPrefix($inlineRelation)); + + // Set the relation property. + $entity->setOriginalProperty($inlineRelation, $propertyValue); + } + } + + // Return parsed entities. + return $entities; + } + + /** + * Map raw object result to an entity. + * @param object $object Raw object. + * @return Entity The entity. + * @throws IncompatibleTypeException + * @throws InvalidTypeException + */ + protected function mapEntity(object $object): Entity + { + // Create a new entity. + $entity = $this->newEntity(); + + // Read entity from SQL properties. + $entity->fromSqlProperties($object); + + // Return parsed entity. + return $entity; + } + + /** + * Eager load given relations. + * @param Entity[] $entities Entities for which to load relations. + * @param array $relationsToLoad Relations to load. + * @return void + * @throws IncompatibleTypeException + * @throws InvalidTypeException + * @throws MissingConditionValueException + * @throws UndefinedRelationException + * @throws UnknownDatabaseException + * @throws UnhandledPropertyTypeException + */ + public function load(array $entities, array $relationsToLoad): void + { + foreach ($relationsToLoad as $relationKey => $relationCallable) + { // For each relation to load... + // Get relation instance. + $relation = $this->entity->getRelation($relationKey); + + // Build base query to retrieve relations. + $query = $relation->queryFor($entities); + + // Apply query modifier callable. + if (is_callable($relationCallable)) + $relationCallable($query); + + // Retrieve models and associate them with the right entities. + $models = $query->get("__reference_key"); + + foreach ($entities as &$entity) + { // Associate the retrieved model(s) to every entity. + if (!empty($models[$entityId = $entity->{array_first_or_val($entity->getPrimaryFields())}])) + $entity->setOriginalProperty($relationKey, $relation->isMultiple() ? $models[$entityId] : array_first($models[$entityId])); + } + } + } + + /** + * Add relations to eager load. + * @param array $relations Relations that should be eager loaded. + * @return $this + */ + public function with(array $relations): static + { + // Normalize relations array and add them in the with array. + array_merge($this->with, EntityQuery::normalizeRelationsDefinition($relations)); + return $this; + } + + /** + * Add relations to NOT eager load. + * @param string[] $relations Relations that shouldn't be eager loaded. + * @return $this + */ + public function without(array $relations): static + { + foreach ($relations as $relation) + { // For each relation to NOT eager load, remove it from the with array. + if (!empty($this->with[$relation])) unset($this->with[$relation]); + } + return $this; + } +} diff --git a/src/ReadableArrayBlueprint.php b/src/ReadableArrayBlueprint.php new file mode 100644 index 0000000..0076005 --- /dev/null +++ b/src/ReadableArrayBlueprint.php @@ -0,0 +1,37 @@ +type; + } + + /** + * Get type instance of the given field. + * @return Type Type instance. + */ + public function getTypeInstance(): Type + { + return new $this->type(); + } + + public function getTable(): ?string + { + return $this->table; + } + + public function getForeignKeyName(): ?string + { + return $this->foreignKeyName; + } + + public function getForeignValueName(): ?string + { + return $this->foreignValueName; + } +} diff --git a/src/ReadableEntityPropertyBlueprint.php b/src/ReadableEntityPropertyBlueprint.php new file mode 100644 index 0000000..d29cb54 --- /dev/null +++ b/src/ReadableEntityPropertyBlueprint.php @@ -0,0 +1,59 @@ +class; + } + + public function isMultiple(): bool + { + return $this->multiple; + } + + public function isInlineAllowed(): bool + { + return $this->allowInline; + } + + public function getMode(): EntityPropertyMode + { + return $this->mode; + } + + public function getLocalKey(): ?string + { + return $this->localKey; + } + + public function getRelatedKey(): ?string + { + return $this->relatedKey; + } + + public function getPivotTable(): ?string + { + return $this->pivotTable; + } + + public function getPivotLocalKey(): ?string + { + return $this->pivotLocalKey; + } + + public function getPivotRelatedKey(): ?string + { + return $this->pivotRelatedKey; + } + + /** + * @return bool True if the field should be eager loaded. + */ + public function doEagerLoad(): bool + { + return $this->eagerLoad; + } +} diff --git a/src/ReadableFieldBlueprint.php b/src/ReadableFieldBlueprint.php new file mode 100644 index 0000000..0702583 --- /dev/null +++ b/src/ReadableFieldBlueprint.php @@ -0,0 +1,62 @@ +type; + } + + /** + * Get type instance of the given field. + * @return Type Type instance. + */ + public function getTypeInstance(): Type + { + return new $this->type(); + } + + public function isPrimary(): bool + { + return $this->primary; + } + + public function isIndexed(): bool + { + return $this->index; + } + + public function isUnique(): bool + { + return $this->unique; + } + + public function isRequired(): bool + { + return $this->required; + } + + public function isAutoIncrementing(): bool + { + return $this->autoIncrement; + } + + public function isUnsigned(): bool + { + return $this->unsigned; + } + + public function hasCurrentDateByDefault(): bool + { + return $this->currentDateByDefault; + } + + public function hasCurrentDateOnUpdate(): bool + { + return $this->currentDateOnUpdate; + } +}