Primo rilascio

This commit is contained in:
2026-03-07 00:15:59 +01:00
commit dd5282dd69
609 changed files with 75246 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
# This is an example configuration file
# To learn more, see the full config.yaml reference: https://docs.continue.dev/reference
name: Example Config
version: 1.0.0
schema: v1
# Define which models can be used
# https://docs.continue.dev/customization/models
models:
- name: my gpt-5
provider: openai
model: gpt-5
apiKey: YOUR_OPENAI_API_KEY_HERE
- uses: ollama/qwen2.5-coder-7b
- uses: anthropic/claude-4-sonnet
with:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# MCP Servers that Continue can access
# https://docs.continue.dev/customization/mcp-tools
mcpServers:
- uses: anthropic/memory-mcp

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"svn.ignoreMissingSvnWarning": true
}

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# NurseryApp - Guida all'avvio
Questo documento ti insegna come avviare sia il **backend** PHP che il **frontend** Angular.
---
## 🚀 **Avviamento del Frontend (Angular)**
1. **Vai nella cartella del frontend:**
```bash
cd frontend/nursery-app
```
2. **Installa le dipendenze (se non ancora fatto):**
```bash
npm install
# oppure
yarn install
```
3. **Avvia lo sviluppo locale:**
```bash
ng serve --host 0.0.0.0
```
🌐 **Apri il browser su:** `http://localhost:4200`
---
## ⚙️ **Avviamento del Backend (PHP)**
1. **Assicurati di avere PHP e Composer installati.**
2. **Vai nella cartella del backend:**
```bash
cd backend
```
3. **Installa le dipendenze PHP:**
```bash
composer install
```
4. **Configura il file `.env`:**
- Se esiste un `.env.example`, copialo:
```bash
cp .env.example .env
```
- Modifica le variabili d'ambiente necessarie (es. DB connection string).
5. **Avvia lo sviluppo del backend:**
### Opzione A: Laravel (se è un progetto Laravel)
```bash
php artisan serve --host=0.0.0.0 --port=8000
```
### Opzione B: PHP semplice
```bash
php -S localhost:8000

12
backend/.env Normal file
View File

@@ -0,0 +1,12 @@
DB_CONNECTION=pdo_mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=nursery_db
DB_USERNAME=root
DB_PASSWORD=
# --- JWT Configuration ---
# In produzione, usare una chiave segreta lunga e casuale generata in modo sicuro
# Esempio: openssl rand -base64 32
JWT_SECRET="your-very-secure-secret-key-here"
JWT_EXPIRATION_TIME=3600 # Scadenza token in secondi (es. 1 ora)

15
backend/composer.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "nidoai/backend",
"description": "Backend API for Nursery Management App",
"require": {
"nikic/fast-route": "^1.3",
"firebase/php-jwt": "^6.11",
"doctrine/dbal": "^4.2",
"vlucas/phpdotenv": "^5.6"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}

845
backend/composer.lock generated Normal file
View File

@@ -0,0 +1,845 @@
{
"_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": "fc7d23307b3d4dd9d5816294e54d026c",
"packages": [
{
"name": "doctrine/dbal",
"version": "4.2.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e",
"reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^0.5.3|^1",
"php": "^8.1",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.2",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "10.5.39",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^6.3.8|^7.0",
"symfony/console": "^5.4|^6.3|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
"abstraction",
"database",
"db2",
"dbal",
"mariadb",
"mssql",
"mysql",
"oci8",
"oracle",
"pdo",
"pgsql",
"postgresql",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/4.2.3"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
"type": "tidelift"
}
],
"time": "2025-03-07T18:29:05+00:00"
},
{
"name": "doctrine/deprecations",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=13"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^13",
"phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
},
"time": "2025-04-07T20:06:18+00:00"
},
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
"time": "2025-04-09T20:32:01+00:00"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2024-07-20T21:45:45+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"type": "library",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"FastRoute\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"support": {
"issues": "https://github.com/nikic/FastRoute/issues",
"source": "https://github.com/nikic/FastRoute/tree/master"
},
"time": "2018-02-13T20:26:39+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2024-07-20T21:41:07+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"time": "2021-02-03T23:26:27+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/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-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": {
"url": "https://github.com/symfony/polyfill",
"name": "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-php80",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/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": "vlucas/phpdotenv",
"version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2024-07-20T21:52:34+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

BIN
backend/composer.phar Normal file

Binary file not shown.

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
return [
'driver' => $_ENV['DB_CONNECTION'] ?? 'pdo_mysql',
'host' => $_ENV['DB_HOST'] ?? '127.0.0.1',
'port' => $_ENV['DB_PORT'] ?? '3306',
'dbname' => $_ENV['DB_DATABASE'] ?? 'nursery_db',
'user' => $_ENV['DB_USERNAME'] ?? 'root',
'password' => $_ENV['DB_PASSWORD'] ?? '',
'charset' => 'utf8mb4',
'driverOptions' => [
// Opzioni specifiche del driver PDO, se necessarie
// PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'
],
];

118
backend/database/schema.sql Normal file
View File

@@ -0,0 +1,118 @@
-- Tabella per le Strutture (Sedi dell'asilo nido)
CREATE TABLE structures (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT 'Nome della struttura',
address VARCHAR(255) NULL COMMENT 'Indirizzo',
city VARCHAR(100) NULL COMMENT 'Città',
province VARCHAR(100) NULL COMMENT 'Provincia',
zip_code VARCHAR(10) NULL COMMENT 'CAP',
phone VARCHAR(50) NULL COMMENT 'Numero di telefono',
email VARCHAR(255) NULL COMMENT 'Indirizzo email',
notes TEXT NULL COMMENT 'Note aggiuntive',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per gli Insegnanti
CREATE TABLE teachers (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(100) NOT NULL COMMENT 'Nome',
last_name VARCHAR(100) NOT NULL COMMENT 'Cognome',
email VARCHAR(255) UNIQUE NULL COMMENT 'Email (opzionale, ma unica se presente)',
phone VARCHAR(50) NULL COMMENT 'Telefono',
date_of_birth DATE NULL COMMENT 'Data di nascita',
hire_date DATE NULL COMMENT 'Data di assunzione',
qualifications TEXT NULL COMMENT 'Qualifiche o note',
is_active BOOLEAN DEFAULT true COMMENT 'Indica se l''insegnante è attualmente attivo',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per gli Anni Scolastici
CREATE TABLE school_years (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE COMMENT 'Nome anno scolastico (es. 2024/2025)',
start_date DATE NOT NULL COMMENT 'Data inizio anno scolastico',
end_date DATE NOT NULL COMMENT 'Data fine anno scolastico',
is_active BOOLEAN DEFAULT false COMMENT 'Indica se è l''anno scolastico corrente/attivo',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT chk_dates CHECK (end_date > start_date) -- Assicura che la fine sia dopo l'inizio
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per i Contratti degli Insegnanti
CREATE TABLE teacher_contracts (
id INT AUTO_INCREMENT PRIMARY KEY,
teacher_id INT NOT NULL COMMENT 'FK a teachers',
school_year_id INT NOT NULL COMMENT 'FK a school_years',
structure_id INT NULL COMMENT 'FK a structures (opzionale, se il contratto è specifico per una struttura)',
contract_type VARCHAR(100) NULL COMMENT 'Tipo di contratto (es. Tempo Indeterminato, Tempo Determinato, Collaborazione)',
start_date DATE NOT NULL COMMENT 'Data inizio validità contratto',
end_date DATE NULL COMMENT 'Data fine validità contratto (NULL se indeterminato)',
weekly_hours DECIMAL(5, 2) NULL COMMENT 'Ore settimanali previste',
salary DECIMAL(10, 2) NULL COMMENT 'Stipendio lordo (mensile/annuale a seconda delle policy)',
notes TEXT NULL COMMENT 'Note aggiuntive sul contratto',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE, -- Se elimino un insegnante, elimino i suoi contratti
FOREIGN KEY (school_year_id) REFERENCES school_years(id) ON DELETE RESTRICT, -- Non posso eliminare un anno scolastico se ha contratti
FOREIGN KEY (structure_id) REFERENCES structures(id) ON DELETE SET NULL, -- Se elimino una struttura, il contratto rimane ma senza struttura associata
UNIQUE KEY unique_teacher_year (teacher_id, school_year_id) -- Un insegnante può avere un solo contratto per anno scolastico? (Da valutare)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per i Bambini
CREATE TABLE children (
id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(100) NOT NULL COMMENT 'Nome',
last_name VARCHAR(100) NOT NULL COMMENT 'Cognome',
date_of_birth DATE NOT NULL COMMENT 'Data di nascita',
enrollment_date DATE NOT NULL COMMENT 'Data di iscrizione',
structure_id INT NULL COMMENT 'FK a structures (struttura di appartenenza principale)',
parent1_name VARCHAR(200) NULL COMMENT 'Nome Genitore 1 / Contatto Principale',
parent1_phone VARCHAR(50) NULL COMMENT 'Telefono Genitore 1',
parent1_email VARCHAR(255) NULL COMMENT 'Email Genitore 1',
parent2_name VARCHAR(200) NULL COMMENT 'Nome Genitore 2 / Contatto Secondario',
parent2_phone VARCHAR(50) NULL COMMENT 'Telefono Genitore 2',
parent2_email VARCHAR(255) NULL COMMENT 'Email Genitore 2',
address VARCHAR(255) NULL COMMENT 'Indirizzo residenza',
city VARCHAR(100) NULL COMMENT 'Città residenza',
notes TEXT NULL COMMENT 'Note (allergie, esigenze particolari, etc.)',
is_active BOOLEAN DEFAULT true COMMENT 'Indica se l''iscrizione è attualmente attiva',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (structure_id) REFERENCES structures(id) ON DELETE SET NULL -- Se elimino struttura, il bambino rimane ma senza struttura
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per le Definizioni dei Turni Standard
CREATE TABLE shift_definitions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE COMMENT 'Nome del turno (es. Mattina, Pomeriggio)',
start_time TIME NOT NULL COMMENT 'Orario di inizio previsto',
end_time TIME NOT NULL COMMENT 'Orario di fine previsto',
notes TEXT NULL COMMENT 'Note aggiuntive',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-- Potremmo aggiungere un flag 'is_default' o 'is_active' se necessario
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabella per gli Utenti (Admin, Operator)
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL COMMENT 'Nome e Cognome utente',
email VARCHAR(255) NOT NULL UNIQUE COMMENT 'Email per login',
password VARCHAR(255) NOT NULL COMMENT 'Password hashata',
role ENUM('admin', 'operator') NOT NULL DEFAULT 'operator' COMMENT 'Ruolo utente',
structure_id INT NULL COMMENT 'FK a structures (se operatore è legato a una struttura specifica)',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (structure_id) REFERENCES structures(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Aggiungi qui le altre tabelle (actual_shifts, etc.)
-- man mano che le definiamo.
-- Aggiungi qui le altre tabelle (children, shifts, users, etc.)
-- man mano che le definiamo.

229
backend/public/index.php Normal file
View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
use App\Database;
use App\Controllers\AuthController;
use App\Controllers\ChildController;
use App\Controllers\SchoolYearController;
use App\Controllers\ShiftDefinitionController;
use App\Controllers\StructureController;
use App\Controllers\TeacherContractController;
use App\Controllers\TeacherController;
use Firebase\JWT\JWT; // Importa JWT
use Firebase\JWT\Key; // Importa Key
use Firebase\JWT\ExpiredException;
use Firebase\JWT\SignatureInvalidException;
// 1. Carica Autoloader Composer
require __DIR__ . '/../vendor/autoload.php';
// 2. Carica Variabili d'Ambiente
try {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
} catch (\Throwable $th) {
error_log("Could not load .env file: " . $th->getMessage());
}
// --- Configurazione JWT ---
$jwtSecret = $_ENV['JWT_SECRET'] ?? 'default-secret-change-me';
// --- Funzione Guardia JWT ---
/**
* Verifica il token JWT dall'header Authorization.
* Restituisce i dati utente dal payload se valido, altrimenti termina con errore 401.
* @return array Dati utente dal payload JWT ('sub', 'data')
*/
function authenticate(): array {
global $jwtSecret; // Rende visibile la chiave segreta definita sopra
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
if (!$authHeader) {
http_response_code(401);
echo json_encode(['error' => 'Authorization header missing']);
exit;
}
// Estrai il token "Bearer <token>"
if (!preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
http_response_code(401);
echo json_encode(['error' => 'Malformed Authorization header']);
exit;
}
$jwt = $matches[1];
if (!$jwt) {
http_response_code(401);
echo json_encode(['error' => 'Access token missing']);
exit;
}
try {
$decoded = JWT::decode($jwt, new Key($jwtSecret, 'HS256'));
// Restituisce l'intero payload decodificato come array associativo
return (array)$decoded;
} catch (ExpiredException $e) {
http_response_code(401);
echo json_encode(['error' => 'Token expired']);
exit;
} catch (SignatureInvalidException $e) {
http_response_code(401);
echo json_encode(['error' => 'Invalid token signature']);
exit;
} catch (\Throwable $e) { // Cattura altri errori JWT
http_response_code(401);
error_log("JWT Decode Error: " . $e->getMessage());
echo json_encode(['error' => 'Invalid token']);
exit;
}
}
// 3. Imposta Headers CORS
$allowedOrigin = $_ENV['FRONTEND_URL'] ?? 'http://localhost:4200';
header("Access-Control-Allow-Origin: " . $allowedOrigin);
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
header("Access-Control-Allow-Credentials: true");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204); exit;
}
header('Content-Type: application/json');
// 4. Definizione Route
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
// --- Public Routes ---
$r->addRoute('POST', '/api/login', [AuthController::class, 'login']);
$r->addRoute('GET', '/api/ping', function () { echo json_encode(['message' => 'pong']); });
// --- Protected Routes (richiedono autenticazione) ---
// Aggiungiamo un gruppo per applicare la guardia a più route
$r->addGroup('/api', function (FastRoute\RouteCollector $r) {
// La guardia verrà chiamata prima dell'handler per queste route
// Route /api/me (protetta)
$r->addRoute('GET', '/me', [AuthController::class, 'me']);
// Structures
$r->addRoute('GET', '/structures', [StructureController::class, 'index']);
$r->addRoute('POST', '/structures', [StructureController::class, 'store']);
$r->addRoute('GET', '/structures/{id:\d+}', [StructureController::class, 'show']);
$r->addRoute('PUT', '/structures/{id:\d+}', [StructureController::class, 'update']);
$r->addRoute('DELETE', '/structures/{id:\d+}', [StructureController::class, 'delete']);
// Teachers
$r->addRoute('GET', '/teachers', [TeacherController::class, 'index']);
$r->addRoute('POST', '/teachers', [TeacherController::class, 'store']);
$r->addRoute('GET', '/teachers/{id:\d+}', [TeacherController::class, 'show']);
$r->addRoute('PUT', '/teachers/{id:\d+}', [TeacherController::class, 'update']);
$r->addRoute('DELETE', '/teachers/{id:\d+}', [TeacherController::class, 'delete']);
// School Years
$r->addRoute('GET', '/school-years', [SchoolYearController::class, 'index']);
$r->addRoute('POST', '/school-years', [SchoolYearController::class, 'store']);
$r->addRoute('GET', '/school-years/{id:\d+}', [SchoolYearController::class, 'show']);
$r->addRoute('PUT', '/school-years/{id:\d+}', [SchoolYearController::class, 'update']);
$r->addRoute('DELETE', '/school-years/{id:\d+}', [SchoolYearController::class, 'delete']);
// Teacher Contracts
$r->addRoute('GET', '/teacher-contracts', [TeacherContractController::class, 'index']);
$r->addRoute('POST', '/teacher-contracts', [TeacherContractController::class, 'store']);
$r->addRoute('GET', '/teacher-contracts/{id:\d+}', [TeacherContractController::class, 'show']);
$r->addRoute('PUT', '/teacher-contracts/{id:\d+}', [TeacherContractController::class, 'update']);
$r->addRoute('DELETE', '/teacher-contracts/{id:\d+}', [TeacherContractController::class, 'delete']);
// Children
$r->addRoute('GET', '/children', [ChildController::class, 'index']);
$r->addRoute('POST', '/children', [ChildController::class, 'store']);
$r->addRoute('GET', '/children/{id:\d+}', [ChildController::class, 'show']);
$r->addRoute('PUT', '/children/{id:\d+}', [ChildController::class, 'update']);
$r->addRoute('DELETE', '/children/{id:\d+}', [ChildController::class, 'delete']);
// Shift Definitions
$r->addRoute('GET', '/shift-definitions', [ShiftDefinitionController::class, 'index']);
$r->addRoute('POST', '/shift-definitions', [ShiftDefinitionController::class, 'store']);
$r->addRoute('GET', '/shift-definitions/{id:\d+}', [ShiftDefinitionController::class, 'show']);
$r->addRoute('PUT', '/shift-definitions/{id:\d+}', [ShiftDefinitionController::class, 'update']);
$r->addRoute('DELETE', '/shift-definitions/{id:\d+}', [ShiftDefinitionController::class, 'delete']);
}); // Fine gruppo /api
});
// 5. Gestione della Richiesta
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
$userData = null; // Inizializza a null
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
http_response_code(404);
echo json_encode(['error' => 'API Endpoint Not Found']);
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed', 'allowed_methods' => $allowedMethods]);
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// --- Applica Guardia JWT se la route è protetta (inizia con /api/ e non è /api/login o /api/ping) ---
if (strpos($uri, '/api/') === 0 && !in_array($uri, ['/api/login', '/api/ping'])) {
try {
$userData = authenticate(); // Verifica token e ottieni dati utente
// Potresti aggiungere qui controllo del ruolo se necessario per specifiche route
// if ($userData['data']->role !== 'admin' && $uri === '/api/users') { ... }
} catch (\Exception $e) {
// L'eccezione è già gestita dentro authenticate() con exit
// Questo blocco catch è per sicurezza, ma non dovrebbe essere raggiunto
http_response_code(500);
error_log("Unexpected error during authentication check: " . $e->getMessage());
echo json_encode(['error' => 'Authentication check failed']);
exit;
}
}
// --- Esecuzione Handler ---
try {
if (is_callable($handler)) {
call_user_func($handler, $vars); // Passa $vars alla closure
} elseif (is_array($handler) && count($handler) === 2 && is_string($handler[0]) && is_string($handler[1])) {
[$class, $method] = $handler;
if (class_exists($class) && method_exists($class, $method)) {
$controller = new $class();
// Passa $vars e $userData (se presente) al metodo del controller
// Modifica i metodi del controller per accettare $userData opzionale se serve
if ($method === 'me' && $userData !== null) { // Caso speciale per AuthController::me
call_user_func([$controller, $method], $userData['data']); // Passa solo il payload 'data'
} else {
call_user_func([$controller, $method], $vars); // Chiamata standard per gli altri
}
} else {
http_response_code(500); error_log("Handler class/method not found: {$class}::{$method}"); echo json_encode(['error' => 'Internal Server Error']);
}
} else {
http_response_code(500); error_log("Invalid handler type defined."); echo json_encode(['error' => 'Internal Server Error']);
}
} catch (\Throwable $e) {
http_response_code(500); error_log("Unhandled error in route handler: " . $e->getMessage() . "\n" . $e->getTraceAsString()); echo json_encode(['error' => 'Internal Server Error']);
}
break;
default:
http_response_code(500); echo json_encode(['error' => 'Internal Server Error - Dispatcher Error']); break;
}

View File

@@ -0,0 +1,10 @@
[06-Mar-2026 21:29:32 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:29:32 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:29:56 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:29:56 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:41 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:41 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:41 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:41 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:50 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90
[06-Mar-2026 21:30:50 UTC] PHP Warning: Array to string conversion in D:\Works\NidoAi\backend\public\index.php on line 90

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Firebase\JWT\JWT; // Importa la libreria JWT
use Firebase\JWT\Key; // Importa Key per la verifica
class AuthController
{
private Connection $db;
private string $jwtSecret;
private int $jwtExpiration;
public function __construct()
{
try {
$this->db = Database::getConnection();
// Leggi la configurazione JWT da .env
$this->jwtSecret = $_ENV['JWT_SECRET'] ?? 'default-secret-change-me'; // Usa un default se non impostato
$this->jwtExpiration = (int)($_ENV['JWT_EXPIRATION_TIME'] ?? 3600); // Default 1 ora
} catch (\Throwable $e) {
error_log("Failed to get DB connection or JWT config in AuthController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - Config']);
exit;
}
}
/**
* Gestisce il login dell'utente.
* POST /api/login
*/
public function login(): void
{
// Prova a leggere JSON, fallback a POST
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
$input = $_POST; // Fallback a POST se JSON non valido o vuoto
}
if (!is_array($input)) { // Se ancora non è un array, errore
http_response_code(400); echo json_encode(['error' => 'Invalid input']); return;
}
$email = $input['email'] ?? null;
$password = $input['password'] ?? null;
if (!$email || !$password) {
http_response_code(400);
echo json_encode(['error' => 'Email and password are required']);
return;
}
try {
// Trova l'utente per email
$user = $this->db->fetchAssociative('SELECT id, name, email, password, role, structure_id, is_active FROM users WHERE email = ?', [$email]);
// Verifica utente e password
if ($user === false || !password_verify($password, $user['password'])) {
http_response_code(401); // Unauthorized
echo json_encode(['error' => 'Invalid credentials']);
return;
}
// Verifica se l'utente è attivo
if (!$user['is_active']) {
http_response_code(403); // Forbidden
echo json_encode(['error' => 'User account is inactive']);
return;
}
// Genera il token JWT
$issuedAt = time();
$expirationTime = $issuedAt + $this->jwtExpiration;
$payload = [
'iat' => $issuedAt, // Issued at: time when the token was generated
'exp' => $expirationTime, // Expiration time
'sub' => $user['id'], // Subject: user ID
'data' => [ // Dati aggiuntivi (payload)
'name' => $user['name'],
'email' => $user['email'],
'role' => $user['role'],
'structure_id' => $user['structure_id']
]
];
$jwt = JWT::encode($payload, $this->jwtSecret, 'HS256');
// Restituisci il token e magari alcuni dati utente
http_response_code(200);
echo json_encode([
'message' => 'Login successful',
'access_token' => $jwt,
'token_type' => 'Bearer',
'expires_in' => $this->jwtExpiration,
'user' => [ // Invia dati utente non sensibili
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
'role' => $user['role'],
'structure_id' => $user['structure_id']
]
]);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Login error: " . $e->getMessage());
echo json_encode(['error' => 'Login failed']);
}
}
/**
* Restituisce i dati dell'utente autenticato.
* GET /api/me (Richiede token valido)
*/
// Questo metodo verrà chiamato DOPO che un middleware/guardia ha verificato il token
// e potenzialmente aggiunto i dati utente alla richiesta (o a un contesto).
// Per ora, assumiamo che $userData sia disponibile dopo la verifica del token.
public function me(array $userData = []): void // $userData verrà iniettato dalla guardia/middleware
{
if (empty($userData)) {
http_response_code(401);
echo json_encode(['error' => 'Not authenticated']);
return;
}
// Restituisce i dati utente estratti dal token (o recuperati dal DB usando l'ID)
http_response_code(200);
echo json_encode($userData); // $userData contiene già i dati dal payload del token
}
// TODO: Implementare register() se necessario, con password_hash()
// TODO: Implementare refresh() per rinnovare il token se necessario
}

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
class ChildController
{
private Connection $db;
public function __construct()
{
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
error_log("Failed to get DB connection in ChildController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di bambini.
* GET /api/children
* Accetta query params: structure_id, is_active
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder()
->select(
'c.id', 'c.first_name', 'c.last_name', 'c.date_of_birth',
'c.enrollment_date', 'c.structure_id', 'c.is_active',
's.name AS structure_name' // Nome struttura associata
)
->from('children', 'c')
->leftJoin('c', 'structures', 's', 'c.structure_id = s.id')
->orderBy('c.last_name', 'ASC')
->addOrderBy('c.first_name', 'ASC');
// Filtri opzionali
if (isset($_GET['structure_id']) && $_GET['structure_id'] !== '') {
$queryBuilder->andWhere('c.structure_id = :structureId')
->setParameter('structureId', $_GET['structure_id']);
}
if (isset($_GET['is_active'])) {
// Converte il valore stringa 'true'/'false' o 1/0 in booleano
$isActive = filter_var($_GET['is_active'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($isActive !== null) {
$queryBuilder->andWhere('c.is_active = :isActive')
->setParameter('isActive', $isActive, \Doctrine\DBAL\ParameterType::BOOLEAN);
}
}
$children = $queryBuilder->fetchAllAssociative();
echo json_encode($children);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching children: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch children']);
}
}
/**
* Mostra un bambino specifico.
* GET /api/children/{id}
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing child ID']); return; }
try {
$queryBuilder = $this->db->createQueryBuilder()
->select('c.*', 's.name AS structure_name')
->from('children', 'c')
->leftJoin('c', 'structures', 's', 'c.structure_id = s.id')
->where('c.id = :id')
->setParameter('id', $id);
$child = $queryBuilder->fetchAssociative();
if ($child === false) {
http_response_code(404);
echo json_encode(['error' => "Child with ID {$id} not found"]);
} else {
echo json_encode($child);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching child {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch child']);
}
}
/**
* Crea un nuovo bambino.
* POST /api/children
*/
public function store(): void
{
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400); echo json_encode(['error' => 'Invalid JSON input']); return;
}
// Validazione campi obbligatori
if (empty($input['first_name']) || empty($input['last_name']) || empty($input['date_of_birth']) || empty($input['enrollment_date'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: first_name, last_name, date_of_birth, enrollment_date']);
return;
}
$dataToInsert = [
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'date_of_birth' => $input['date_of_birth'], // Assumiamo YYYY-MM-DD
'enrollment_date' => $input['enrollment_date'], // Assumiamo YYYY-MM-DD
'structure_id' => (isset($input['structure_id']) && $input['structure_id'] !== '') ? (int)$input['structure_id'] : null,
'parent1_name' => $input['parent1_name'] ?? null,
'parent1_phone' => $input['parent1_phone'] ?? null,
'parent1_email' => $input['parent1_email'] ?? null,
'parent2_name' => $input['parent2_name'] ?? null,
'parent2_phone' => $input['parent2_phone'] ?? null,
'parent2_email' => $input['parent2_email'] ?? null,
'address' => $input['address'] ?? null,
'city' => $input['city'] ?? null,
'notes' => $input['notes'] ?? null,
'is_active' => isset($input['is_active']) ? (bool)$input['is_active'] : true, // Default a true
];
try {
$result = $this->db->insert('children', $dataToInsert);
if ($result === false || $result === 0) { throw new \Exception("Failed to insert child."); }
$newId = $this->db->lastInsertId();
http_response_code(201);
$this->show(['id' => $newId]); // Mostra il bambino appena creato
} catch (ForeignKeyConstraintViolationException $e) {
http_response_code(400);
error_log("Error creating child (invalid structure ID?): " . $e->getMessage());
echo json_encode(['error' => 'Invalid structure ID provided.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating child: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create child']);
}
}
/**
* Aggiorna un bambino esistente.
* PUT /api/children/{id}
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing child ID']); return; }
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400); echo json_encode(['error' => 'Invalid JSON input']); return;
}
if (empty($input)) { http_response_code(400); echo json_encode(['error' => 'Missing update data']); return; }
$dataToUpdate = [];
$allowedFields = [
'first_name', 'last_name', 'date_of_birth', 'enrollment_date', 'structure_id',
'parent1_name', 'parent1_phone', 'parent1_email',
'parent2_name', 'parent2_phone', 'parent2_email',
'address', 'city', 'notes', 'is_active'
];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
$value = $input[$field];
if ($field === 'is_active') {
$dataToUpdate[$field] = (bool)$value;
} elseif ($field === 'structure_id' && ($value === '' || $value === null)) {
$dataToUpdate[$field] = null;
} elseif ($field === 'structure_id') {
$dataToUpdate[$field] = (int)$value;
} else {
$dataToUpdate[$field] = ($value === '') ? null : $value; // Imposta null se stringa vuota per altri campi opzionali
}
}
}
if (empty($dataToUpdate)) { http_response_code(400); echo json_encode(['error' => 'No valid fields provided for update']); return; }
try {
$existing = $this->db->fetchOne('SELECT 1 FROM children WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Child with ID {$id} not found"]); return; }
$this->db->update('children', $dataToUpdate, ['id' => $id]);
$this->show(['id' => $id]); // Mostra il bambino aggiornato
} catch (ForeignKeyConstraintViolationException $e) {
http_response_code(400);
error_log("Error updating child {$id} (invalid structure ID?): " . $e->getMessage());
echo json_encode(['error' => 'Invalid structure ID provided.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating child {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update child']);
}
}
/**
* Elimina un bambino.
* DELETE /api/children/{id}
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing child ID']); return; }
try {
// Aggiungere qui controlli se il bambino è referenziato altrove (es. presenze, pagamenti)
// prima di permettere l'eliminazione.
$existing = $this->db->fetchOne('SELECT 1 FROM children WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Child with ID {$id} not found"]); return; }
$deletedRows = $this->db->delete('children', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content
} else {
http_response_code(500);
error_log("Failed to delete child {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete child']);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error deleting child {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete child']);
}
}
}

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
class SchoolYearController
{
private Connection $db;
public function __construct()
{
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
error_log("Failed to get DB connection in SchoolYearController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di anni scolastici.
* GET /api/school-years
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder();
// Ordina per data di inizio decrescente per vedere i più recenti prima
$schoolYears = $queryBuilder
->select('*')
->from('school_years')
->orderBy('start_date', 'ASC')
->fetchAllAssociative();
echo json_encode($schoolYears);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching school years: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch school years']);
}
}
/**
* Mostra un anno scolastico specifico.
* GET /api/school-years/{id}
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing school year ID']);
return;
}
try {
$schoolYear = $this->db->fetchAssociative('SELECT * FROM school_years WHERE id = ?', [$id]);
if ($schoolYear === false) {
http_response_code(404);
echo json_encode(['error' => "School year with ID {$id} not found"]);
} else {
echo json_encode($schoolYear);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching school year {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch school year']);
}
}
/**
* Crea un nuovo anno scolastico.
* POST /api/school-years
*/
public function store(): void
{
$input = (array) json_decode(file_get_contents('php://input'), true);
if (empty($input) && !empty($_POST)) { $input = $_POST; }
// Validazione
if (empty($input['name']) || empty($input['start_date']) || empty($input['end_date'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, start_date, end_date']);
return;
}
if (strtotime($input['end_date']) <= strtotime($input['start_date'])) {
http_response_code(400);
echo json_encode(['error' => 'End date must be after start date']);
return;
}
$dataToInsert = [
'name' => $input['name'],
'start_date' => $input['start_date'], // Assumiamo formato YYYY-MM-DD
'end_date' => $input['end_date'], // Assumiamo formato YYYY-MM-DD
'is_active' => isset($input['is_active']) ? (bool)$input['is_active'] : false, // Default a false
];
try {
// Se is_active è true, assicurati che nessun altro anno sia attivo
if ($dataToInsert['is_active']) {
$this->db->executeStatement('UPDATE school_years SET is_active = false WHERE is_active = true');
}
$result = $this->db->insert('school_years', $dataToInsert);
if ($result === false || $result === 0) {
throw new \Exception("Failed to insert school year.");
}
$newId = $this->db->lastInsertId();
http_response_code(201);
$newSchoolYear = $this->db->fetchAssociative('SELECT * FROM school_years WHERE id = ?', [$newId]);
echo json_encode($newSchoolYear);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409); // Conflict
error_log("Error creating school year (duplicate name?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to create school year. Name might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating school year: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create school year']);
}
}
/**
* Aggiorna un anno scolastico esistente.
* PUT /api/school-years/{id}
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing school year ID']);
return;
}
$inputJson = file_get_contents('php://input');
$input = $inputJson ? (array) json_decode($inputJson, true) : [];
if (empty($input) && $inputJson && ($_SERVER['CONTENT_TYPE'] ?? '') === 'application/x-www-form-urlencoded') {
parse_str($inputJson, $input);
}
if (empty($input)) {
http_response_code(400);
echo json_encode(['error' => 'Missing update data']);
return;
}
// Validazione date se presenti
$startDate = $input['start_date'] ?? null;
$endDate = $input['end_date'] ?? null;
// Se vengono fornite entrambe le date, valida la loro coerenza
if ($startDate && $endDate && strtotime($endDate) <= strtotime($startDate)) {
http_response_code(400);
echo json_encode(['error' => 'End date must be after start date']);
return;
}
// Se viene fornita solo una data, recupera l'altra dal DB per validare
if (($startDate && !$endDate) || (!$startDate && $endDate)) {
$currentDates = $this->db->fetchAssociative('SELECT start_date, end_date FROM school_years WHERE id = ?', [$id]);
if ($currentDates) {
if ($startDate && strtotime($currentDates['end_date']) <= strtotime($startDate)) {
http_response_code(400); echo json_encode(['error' => 'Start date must be before existing end date']); return;
}
if ($endDate && strtotime($endDate) <= strtotime($currentDates['start_date'])) {
http_response_code(400); echo json_encode(['error' => 'End date must be after existing start date']); return;
}
}
}
$dataToUpdate = [];
$allowedFields = ['name', 'start_date', 'end_date', 'is_active'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
if ($field === 'is_active') {
$dataToUpdate[$field] = (bool)$input[$field];
} else {
$dataToUpdate[$field] = $input[$field];
}
}
}
if (empty($dataToUpdate)) {
http_response_code(400);
echo json_encode(['error' => 'No valid fields provided for update']);
return;
}
try {
$existing = $this->db->fetchOne('SELECT 1 FROM school_years WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "School year with ID {$id} not found"]);
return;
}
// Se si sta attivando questo anno, disattiva gli altri
if (isset($dataToUpdate['is_active']) && $dataToUpdate['is_active']) {
$this->db->executeStatement('UPDATE school_years SET is_active = false WHERE is_active = true AND id != ?', [$id]);
}
$this->db->update('school_years', $dataToUpdate, ['id' => $id]);
$updatedSchoolYear = $this->db->fetchAssociative('SELECT * FROM school_years WHERE id = ?', [$id]);
echo json_encode($updatedSchoolYear);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409); // Conflict
error_log("Error updating school year {$id} (duplicate name?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to update school year. Name might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating school year {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update school year']);
}
}
/**
* Elimina un anno scolastico.
* DELETE /api/school-years/{id}
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing school year ID']);
return;
}
try {
// Aggiungere qui controlli se l'anno scolastico è referenziato altrove (es. contratti)
// prima di permettere l'eliminazione.
// Esempio: $count = $this->db->fetchOne('SELECT COUNT(*) FROM contracts WHERE school_year_id = ?', [$id]);
// if ($count > 0) { http_response_code(409); echo json_encode(...); return; }
$existing = $this->db->fetchOne('SELECT 1 FROM school_years WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "School year with ID {$id} not found"]);
return;
}
$deletedRows = $this->db->delete('school_years', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content
} else {
http_response_code(500);
error_log("Failed to delete school year {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete school year']);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error deleting school year {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete school year']);
}
}
}

View File

@@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
class ShiftDefinitionController
{
private Connection $db;
public function __construct()
{
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
error_log("Failed to get DB connection in ShiftDefinitionController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di definizioni di turno.
* GET /api/shift-definitions
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder();
$shifts = $queryBuilder
->select('*')
->from('shift_definitions')
->orderBy('start_time', 'ASC') // Ordina per orario inizio
->fetchAllAssociative();
echo json_encode($shifts);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching shift definitions: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch shift definitions']);
}
}
/**
* Mostra una definizione di turno specifica.
* GET /api/shift-definitions/{id}
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing shift definition ID']); return; }
try {
$shift = $this->db->fetchAssociative('SELECT * FROM shift_definitions WHERE id = ?', [$id]);
if ($shift === false) {
http_response_code(404);
echo json_encode(['error' => "Shift definition with ID {$id} not found"]);
} else {
echo json_encode($shift);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching shift definition {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch shift definition']);
}
}
/**
* Crea una nuova definizione di turno.
* POST /api/shift-definitions
*/
public function store(): void
{
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400); echo json_encode(['error' => 'Invalid JSON input']); return;
}
// Validazione campi obbligatori
if (empty($input['name']) || empty($input['start_time']) || empty($input['end_time'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, start_time, end_time']);
return;
}
// Validazione formato orario (HH:MM o HH:MM:SS)
$timeRegex = '/^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/';
if (!preg_match($timeRegex, $input['start_time']) || !preg_match($timeRegex, $input['end_time'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid time format. Use HH:MM or HH:MM:SS.']);
return;
}
// Validazione logica orari (opzionale, ma utile)
if (strtotime($input['end_time']) <= strtotime($input['start_time'])) {
http_response_code(400);
echo json_encode(['error' => 'End time must be after start time.']);
return;
}
$dataToInsert = [
'name' => $input['name'],
'start_time' => $input['start_time'],
'end_time' => $input['end_time'],
'notes' => $input['notes'] ?? null,
];
try {
$result = $this->db->insert('shift_definitions', $dataToInsert);
if ($result === false || $result === 0) { throw new \Exception("Failed to insert shift definition."); }
$newId = $this->db->lastInsertId();
http_response_code(201);
$this->show(['id' => $newId]);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409);
error_log("Error creating shift definition (duplicate name?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to create shift definition. Name might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating shift definition: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create shift definition']);
}
}
/**
* Aggiorna una definizione di turno esistente.
* PUT /api/shift-definitions/{id}
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing shift definition ID']); return; }
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400); echo json_encode(['error' => 'Invalid JSON input']); return;
}
if (empty($input)) { http_response_code(400); echo json_encode(['error' => 'Missing update data']); return; }
// Validazione formato orario se presenti
$timeRegex = '/^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$/';
if (isset($input['start_time']) && !preg_match($timeRegex, $input['start_time'])) {
http_response_code(400); echo json_encode(['error' => 'Invalid start_time format.']); return;
}
if (isset($input['end_time']) && !preg_match($timeRegex, $input['end_time'])) {
http_response_code(400); echo json_encode(['error' => 'Invalid end_time format.']); return;
}
// Validazione logica orari se entrambi presenti
$startTime = $input['start_time'] ?? null;
$endTime = $input['end_time'] ?? null;
if ($startTime && $endTime && strtotime($endTime) <= strtotime($startTime)) {
http_response_code(400); echo json_encode(['error' => 'End time must be after start time.']); return;
}
// Validazione con orari esistenti se solo uno viene fornito (più complessa, omessa per brevità)
$dataToUpdate = [];
$allowedFields = ['name', 'start_time', 'end_time', 'notes'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
$dataToUpdate[$field] = ($input[$field] === '') ? null : $input[$field];
}
}
if (empty($dataToUpdate)) { http_response_code(400); echo json_encode(['error' => 'No valid fields provided for update']); return; }
try {
$existing = $this->db->fetchOne('SELECT 1 FROM shift_definitions WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Shift definition with ID {$id} not found"]); return; }
$this->db->update('shift_definitions', $dataToUpdate, ['id' => $id]);
$this->show(['id' => $id]);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409);
error_log("Error updating shift definition {$id} (duplicate name?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to update shift definition. Name might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating shift definition {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update shift definition']);
}
}
/**
* Elimina una definizione di turno.
* DELETE /api/shift-definitions/{id}
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing shift definition ID']); return; }
try {
// Aggiungere qui controlli se la definizione è usata in turni effettivi?
$existing = $this->db->fetchOne('SELECT 1 FROM shift_definitions WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Shift definition with ID {$id} not found"]); return; }
$deletedRows = $this->db->delete('shift_definitions', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content
} else {
http_response_code(500);
error_log("Failed to delete shift definition {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete shift definition']);
}
} catch (\Throwable $e) {
// Gestire ForeignKeyConstraintViolationException se la definizione è usata altrove
http_response_code(500);
error_log("Error deleting shift definition {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete shift definition. It might be in use.']);
}
}
}

View File

@@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
class StructureController
{
private Connection $db;
public function __construct()
{
// Ottieni la connessione al database
// In un'applicazione reale, useresti Dependency Injection
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
// Gestisci l'errore di connessione iniziale se necessario
error_log("Failed to get DB connection in StructureController: " . $e->getMessage());
// Potresti voler lanciare un'eccezione o impostare uno stato di errore
// Per ora, usciamo o lanciamo un errore HTTP 500 se la connessione fallisce qui
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di strutture.
* GET /api/structures
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder();
$structures = $queryBuilder
->select('*')
->from('structures')
->orderBy('name', 'ASC') // Ordina per nome
->fetchAllAssociative(); // Recupera tutti i risultati come array associativi
echo json_encode($structures);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching structures: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch structures']);
}
}
/**
* Mostra una struttura specifica.
* GET /api/structures/{id}
* @param array $vars Variabili dalla route (es. ['id' => '1'])
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400); // Bad Request
echo json_encode(['error' => 'Missing structure ID']);
return;
}
try {
// Usa fetchAssociative per recuperare una singola riga
$structure = $this->db->fetchAssociative('SELECT * FROM structures WHERE id = ?', [$id]);
if ($structure === false) {
// Nessuna struttura trovata con quell'ID
http_response_code(404); // Not Found
echo json_encode(['error' => "Structure with ID {$id} not found"]);
} else {
// Struttura trovata, restituiscila
echo json_encode($structure);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching structure {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch structure']);
}
}
/**
* Crea una nuova struttura.
* POST /api/structures
*/
public function store(): void
{
// Prova a leggere da php://input (per application/json)
$inputJson = file_get_contents('php://input');
$input = $inputJson ? (array) json_decode($inputJson, true) : [];
// Se php://input è vuoto o non è JSON valido, prova a leggere da $_POST (per form-data)
if (empty($input) && !empty($_POST)) {
$input = $_POST;
}
// Validazione semplice (solo il nome è obbligatorio per ora)
if (empty($input['name'])) {
http_response_code(400); // Bad Request
echo json_encode(['error' => 'Missing required field: name']);
return;
}
// Prepara i dati per l'inserimento (includi solo i campi validi per la tabella)
$dataToInsert = [
'name' => $input['name'],
'address' => $input['address'] ?? null,
'city' => $input['city'] ?? null,
'province' => $input['province'] ?? null,
'zip_code' => $input['zip_code'] ?? null,
'phone' => $input['phone'] ?? null,
'email' => $input['email'] ?? null,
'notes' => $input['notes'] ?? null,
// created_at e updated_at sono gestiti dal DB
];
try {
$result = $this->db->insert('structures', $dataToInsert);
if ($result === false || $result === 0) {
// Doctrine DBAL < 3.3 restituisce false, >= 3.3 restituisce 0 in caso di fallimento senza eccezione
throw new \Exception("Failed to insert structure, insert returned non-true value.");
}
$newId = $this->db->lastInsertId();
http_response_code(201); // Created
// Recupera e restituisci la risorsa appena creata
$newStructure = $this->db->fetchAssociative('SELECT * FROM structures WHERE id = ?', [$newId]);
echo json_encode($newStructure);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating structure: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create structure']);
}
}
/**
* Aggiorna una struttura esistente.
* PUT /api/structures/{id}
* @param array $vars Variabili dalla route (es. ['id' => '1'])
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing structure ID']);
return;
}
// Leggi i dati (prova JSON e poi POST/form-urlencoded)
$inputJson = file_get_contents('php://input');
$input = $inputJson ? (array) json_decode($inputJson, true) : [];
// Per PUT con form-urlencoded, php://input contiene la stringa query
if (empty($input) && $inputJson && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded') {
parse_str($inputJson, $input);
}
if (empty($input)) {
http_response_code(400);
echo json_encode(['error' => 'Missing update data']);
return;
}
// Prepara i dati per l'aggiornamento (solo i campi validi e presenti nell'input)
$dataToUpdate = [];
$allowedFields = ['name', 'address', 'city', 'province', 'zip_code', 'phone', 'email', 'notes'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
// Potresti aggiungere qui ulteriore validazione per tipo/formato
$dataToUpdate[$field] = $input[$field];
}
}
if (empty($dataToUpdate)) {
http_response_code(400);
echo json_encode(['error' => 'No valid fields provided for update']);
return;
}
try {
// Verifica prima se la risorsa esiste
$existing = $this->db->fetchOne('SELECT 1 FROM structures WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "Structure with ID {$id} not found"]);
return;
}
// Esegui l'aggiornamento
$this->db->update('structures', $dataToUpdate, ['id' => $id]);
// Recupera e restituisci la risorsa aggiornata
$updatedStructure = $this->db->fetchAssociative('SELECT * FROM structures WHERE id = ?', [$id]);
echo json_encode($updatedStructure);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating structure {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update structure']);
}
}
/**
* Elimina una struttura.
* DELETE /api/structures/{id}
* @param array $vars Variabili dalla route (es. ['id' => '1'])
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing structure ID']);
return;
}
try {
// Verifica prima se la risorsa esiste
$existing = $this->db->fetchOne('SELECT 1 FROM structures WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "Structure with ID {$id} not found"]);
return;
}
// Esegui l'eliminazione
$deletedRows = $this->db->delete('structures', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content - Successo
// Non c'è corpo della risposta per 204
} else {
// Questo non dovrebbe accadere se la verifica precedente ha funzionato
http_response_code(500);
error_log("Failed to delete structure {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete structure']);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error deleting structure {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete structure']);
}
}
}

View File

@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
class TeacherContractController
{
private Connection $db;
public function __construct()
{
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
error_log("Failed to get DB connection in TeacherContractController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di contratti con nomi associati.
* GET /api/teacher-contracts
* Accetta query params: teacher_id, school_year_id, structure_id
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder()
->select(
'tc.id', 'tc.teacher_id', 'tc.school_year_id', 'tc.structure_id',
'tc.contract_type', 'tc.start_date', 'tc.end_date', 'tc.weekly_hours', 'tc.salary',
't.first_name AS teacher_first_name', 't.last_name AS teacher_last_name',
'sy.name AS school_year_name',
's.name AS structure_name'
)
->from('teacher_contracts', 'tc')
->leftJoin('tc', 'teachers', 't', 'tc.teacher_id = t.id')
->leftJoin('tc', 'school_years', 'sy', 'tc.school_year_id = sy.id')
->leftJoin('tc', 'structures', 's', 'tc.structure_id = s.id')
->orderBy('tc.start_date', 'DESC');
// Filtri opzionali
if (!empty($_GET['teacher_id'])) {
$queryBuilder->andWhere('tc.teacher_id = :teacherId')
->setParameter('teacherId', $_GET['teacher_id']);
}
if (!empty($_GET['school_year_id'])) {
$queryBuilder->andWhere('tc.school_year_id = :schoolYearId')
->setParameter('schoolYearId', $_GET['school_year_id']);
}
if (!empty($_GET['structure_id'])) {
$queryBuilder->andWhere('tc.structure_id = :structureId')
->setParameter('structureId', $_GET['structure_id']);
}
$contracts = $queryBuilder->fetchAllAssociative();
echo json_encode($contracts);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching teacher contracts: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch teacher contracts']);
}
}
/**
* Mostra un contratto specifico con nomi associati.
* GET /api/teacher-contracts/{id}
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400); echo json_encode(['error' => 'Missing contract ID']); return;
}
try {
$queryBuilder = $this->db->createQueryBuilder()
->select(
'tc.*',
't.first_name AS teacher_first_name', 't.last_name AS teacher_last_name',
'sy.name AS school_year_name',
's.name AS structure_name'
)
->from('teacher_contracts', 'tc')
->leftJoin('tc', 'teachers', 't', 'tc.teacher_id = t.id')
->leftJoin('tc', 'school_years', 'sy', 'tc.school_year_id = sy.id')
->leftJoin('tc', 'structures', 's', 'tc.structure_id = s.id')
->where('tc.id = :id')
->setParameter('id', $id);
$contract = $queryBuilder->fetchAssociative();
if ($contract === false) {
http_response_code(404);
echo json_encode(['error' => "Teacher contract with ID {$id} not found"]);
} else {
echo json_encode($contract);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching teacher contract {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch teacher contract']);
}
}
/**
* Crea un nuovo contratto.
* POST /api/teacher-contracts
*/
public function store(): void
{
// Leggi il corpo JSON della richiesta
$input = json_decode(file_get_contents('php://input'), true);
// Verifica se il JSON è valido e se è un array
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON input']);
return;
}
// Validazione campi obbligatori
if (empty($input['teacher_id']) || empty($input['school_year_id']) || empty($input['start_date'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: teacher_id, school_year_id, start_date']);
return;
}
// Validazione date rimossa - affidata a frontend/DB constraint
error_log(json_encode($input));
$dataToInsert = [
'teacher_id' => $input['teacher_id'],
'school_year_id' => $input['school_year_id'],
'structure_id' => (isset($input['structure_id']) && $input['structure_id'] !== '' && $input['structure_id'] !== null) ? (int)$input['structure_id'] : null,
'contract_type' => $input['contract_type'] ?? null,
'start_date' => $input['start_date'],
'end_date' => (isset($input['end_date']) && $input['end_date'] !== '') ? $input['end_date'] : null, // Forza null se vuoto/non presente
'weekly_hours' => (isset($input['weekly_hours']) && $input['weekly_hours'] !== '') ? (float)$input['weekly_hours'] : null, // Forza null se vuoto/non presente
'salary' => (isset($input['salary']) && $input['salary'] !== '') ? (float)$input['salary'] : null, // Forza null se vuoto/non presente
'notes' => $input['notes'] ?? null,
];
try {
$result = $this->db->insert('teacher_contracts', $dataToInsert);
if ($result === false || $result === 0) { throw new \Exception("Failed to insert teacher contract."); }
$newId = $this->db->lastInsertId();
http_response_code(201);
$this->show(['id' => $newId]); // Mostra il contratto appena creato
} catch (UniqueConstraintViolationException $e) {
http_response_code(409);
error_log("Error creating teacher contract (duplicate teacher/year?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to create contract. A contract for this teacher and school year might already exist.']);
} catch (ForeignKeyConstraintViolationException $e) {
http_response_code(400);
error_log("Error creating teacher contract (invalid teacher/year/structure ID?): " . $e->getMessage());
echo json_encode(['error' => 'Invalid teacher, school year, or structure ID provided.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating teacher contract: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create teacher contract']);
}
}
/**
* Aggiorna un contratto esistente.
* PUT /api/teacher-contracts/{id}
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing contract ID']); return; }
// Leggi il corpo JSON della richiesta
$input = json_decode(file_get_contents('php://input'), true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($input)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON input']);
return;
}
if (empty($input)) { http_response_code(400); echo json_encode(['error' => 'Missing update data']); return; }
$dataToUpdate = [];
$allowedFields = ['teacher_id', 'school_year_id', 'structure_id', 'contract_type', 'start_date', 'end_date', 'weekly_hours', 'salary', 'notes'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
$value = $input[$field];
// Gestione specifica per i campi che possono essere NULL
if (in_array($field, ['structure_id', 'end_date', 'weekly_hours', 'salary', 'notes'])) {
if ($value === '' || $value === null) {
$dataToUpdate[$field] = null;
} elseif ($field === 'structure_id') {
$dataToUpdate[$field] = (int)$value; // Cast a int se non null/vuoto
} elseif ($field === 'weekly_hours' || $field === 'salary') {
$dataToUpdate[$field] = (float)$value; // Cast a float se non null/vuoto
} else {
$dataToUpdate[$field] = $value; // Per end_date, contract_type, notes
}
} else { // Campi obbligatori (teacher_id, school_year_id, start_date)
$dataToUpdate[$field] = $value;
}
}
}
if (empty($dataToUpdate)) { http_response_code(400); echo json_encode(['error' => 'No valid fields provided for update']); return; }
try {
$existing = $this->db->fetchOne('SELECT 1 FROM teacher_contracts WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Contract with ID {$id} not found"]); return; }
$this->db->update('teacher_contracts', $dataToUpdate, ['id' => $id]);
$this->show(['id' => $id]); // Mostra il contratto aggiornato
} catch (UniqueConstraintViolationException $e) {
http_response_code(409);
error_log("Error updating teacher contract {$id} (duplicate teacher/year?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to update contract. A contract for this teacher and school year might already exist.']);
} catch (ForeignKeyConstraintViolationException $e) {
http_response_code(400);
error_log("Error updating teacher contract {$id} (invalid teacher/year/structure ID?): " . $e->getMessage());
echo json_encode(['error' => 'Invalid teacher, school year, or structure ID provided.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating teacher contract {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update teacher contract']);
}
}
/**
* Elimina un contratto.
* DELETE /api/teacher-contracts/{id}
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) { http_response_code(400); echo json_encode(['error' => 'Missing contract ID']); return; }
try {
$existing = $this->db->fetchOne('SELECT 1 FROM teacher_contracts WHERE id = ?', [$id]);
if ($existing === false) { http_response_code(404); echo json_encode(['error' => "Contract with ID {$id} not found"]); return; }
$deletedRows = $this->db->delete('teacher_contracts', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content
} else {
http_response_code(500);
error_log("Failed to delete teacher contract {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete teacher contract']);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error deleting teacher contract {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete teacher contract']);
}
}
}

View File

@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
class TeacherController
{
private Connection $db;
public function __construct()
{
try {
$this->db = Database::getConnection();
} catch (\Throwable $e) {
error_log("Failed to get DB connection in TeacherController: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 'Internal Server Error - DB Connection']);
exit;
}
}
/**
* Mostra un elenco di insegnanti.
* GET /api/teachers
*/
public function index(): void
{
try {
$queryBuilder = $this->db->createQueryBuilder();
$teachers = $queryBuilder
->select('id', 'first_name', 'last_name', 'email', 'phone', 'is_active') // Seleziona solo alcuni campi per la lista
->from('teachers')
->orderBy('last_name', 'ASC')
->addOrderBy('first_name', 'ASC')
->fetchAllAssociative();
echo json_encode($teachers);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching teachers: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch teachers']);
}
}
/**
* Mostra un insegnante specifico.
* GET /api/teachers/{id}
*/
public function show(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing teacher ID']);
return;
}
try {
$teacher = $this->db->fetchAssociative('SELECT * FROM teachers WHERE id = ?', [$id]);
if ($teacher === false) {
http_response_code(404);
echo json_encode(['error' => "Teacher with ID {$id} not found"]);
} else {
echo json_encode($teacher);
}
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error fetching teacher {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to fetch teacher']);
}
}
/**
* Crea un nuovo insegnante.
* POST /api/teachers
*/
public function store(): void
{
$input = (array) json_decode(file_get_contents('php://input'), true);
if (empty($input) && !empty($_POST)) { $input = $_POST; }
// Validazione di base
if (empty($input['first_name']) || empty($input['last_name'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: first_name, last_name']);
return;
}
$dataToInsert = [
'first_name' => $input['first_name'],
'last_name' => $input['last_name'],
'email' => $input['email'] ?? null,
'phone' => $input['phone'] ?? null,
'date_of_birth' => !empty($input['date_of_birth']) ? $input['date_of_birth'] : null,
'hire_date' => !empty($input['hire_date']) ? $input['hire_date'] : null,
'qualifications' => $input['qualifications'] ?? null,
'is_active' => isset($input['is_active']) ? (bool)$input['is_active'] : true,
];
try {
$result = $this->db->insert('teachers', $dataToInsert);
if ($result === false || $result === 0) {
throw new \Exception("Failed to insert teacher, insert returned non-true value.");
}
$newId = $this->db->lastInsertId();
http_response_code(201);
$newTeacher = $this->db->fetchAssociative('SELECT * FROM teachers WHERE id = ?', [$newId]);
echo json_encode($newTeacher);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409); // Conflict
error_log("Error creating teacher (duplicate email?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to create teacher. Email might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error creating teacher: " . $e->getMessage());
echo json_encode(['error' => 'Failed to create teacher']);
}
}
/**
* Aggiorna un insegnante esistente.
* PUT /api/teachers/{id}
*/
public function update(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing teacher ID']);
return;
}
$inputJson = file_get_contents('php://input');
$input = $inputJson ? (array) json_decode($inputJson, true) : [];
if (empty($input) && $inputJson && ($_SERVER['CONTENT_TYPE'] ?? '') === 'application/x-www-form-urlencoded') {
parse_str($inputJson, $input);
}
if (empty($input)) {
http_response_code(400);
echo json_encode(['error' => 'Missing update data']);
return;
}
$dataToUpdate = [];
$allowedFields = ['first_name', 'last_name', 'email', 'phone', 'date_of_birth', 'hire_date', 'qualifications', 'is_active'];
foreach ($allowedFields as $field) {
if (array_key_exists($field, $input)) {
// Gestione specifica per booleano e date nulle
if ($field === 'is_active') {
$dataToUpdate[$field] = (bool)$input[$field];
} elseif (($field === 'date_of_birth' || $field === 'hire_date') && empty($input[$field])) {
$dataToUpdate[$field] = null;
} else {
$dataToUpdate[$field] = $input[$field];
}
}
}
if (empty($dataToUpdate)) {
http_response_code(400);
echo json_encode(['error' => 'No valid fields provided for update']);
return;
}
try {
$existing = $this->db->fetchOne('SELECT 1 FROM teachers WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "Teacher with ID {$id} not found"]);
return;
}
$this->db->update('teachers', $dataToUpdate, ['id' => $id]);
$updatedTeacher = $this->db->fetchAssociative('SELECT * FROM teachers WHERE id = ?', [$id]);
echo json_encode($updatedTeacher);
} catch (UniqueConstraintViolationException $e) {
http_response_code(409); // Conflict
error_log("Error updating teacher {$id} (duplicate email?): " . $e->getMessage());
echo json_encode(['error' => 'Failed to update teacher. Email might already exist.']);
} catch (\Throwable $e) {
http_response_code(500);
error_log("Error updating teacher {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to update teacher']);
}
}
/**
* Elimina un insegnante.
* DELETE /api/teachers/{id}
*/
public function delete(array $vars): void
{
$id = $vars['id'] ?? null;
if ($id === null) {
http_response_code(400);
echo json_encode(['error' => 'Missing teacher ID']);
return;
}
try {
$existing = $this->db->fetchOne('SELECT 1 FROM teachers WHERE id = ?', [$id]);
if ($existing === false) {
http_response_code(404);
echo json_encode(['error' => "Teacher with ID {$id} not found"]);
return;
}
$deletedRows = $this->db->delete('teachers', ['id' => $id]);
if ($deletedRows > 0) {
http_response_code(204); // No Content
} else {
http_response_code(500);
error_log("Failed to delete teacher {$id}, delete returned 0 rows affected.");
echo json_encode(['error' => 'Failed to delete teacher']);
}
} catch (\Throwable $e) {
// Considera gestione Foreign Key Constraints se l'insegnante è referenziato altrove
http_response_code(500);
error_log("Error deleting teacher {$id}: " . $e->getMessage());
echo json_encode(['error' => 'Failed to delete teacher']);
}
}
}

42
backend/src/Database.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Connection;
class Database
{
private static ?Connection $connection = null;
/**
* Ottiene l'istanza della connessione al database (Singleton).
*
* @return Connection
* @throws \Doctrine\DBAL\Exception
*/
public static function getConnection(): Connection
{
if (self::$connection === null) {
// Carica la configurazione dal file
$connectionParams = require __DIR__ . '/../config/database.php';
// Crea la configurazione di Doctrine (opzionale, per ora vuota)
$config = new Configuration();
// Crea la connessione
self::$connection = DriverManager::getConnection($connectionParams, $config);
}
return self::$connection;
}
// Impedisce la clonazione dell'istanza
private function __clone() {}
// Impedisce la deserializzazione dell'istanza
public function __wakeup() {}
}

22
backend/vendor/autoload.php vendored Normal file
View File

@@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit12c1165ec2a392b21c0fbfeb2574a7bb::getLoader();

579
backend/vendor/composer/ClassLoader.php vendored Normal file
View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
backend/vendor/composer/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,15 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

View File

@@ -0,0 +1,13 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,22 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
'FastRoute\\' => array($vendorDir . '/nikic/fast-route/src'),
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'),
'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'),
'App\\' => array($baseDir . '/src'),
);

View File

@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit12c1165ec2a392b21c0fbfeb2574a7bb
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit12c1165ec2a392b21c0fbfeb2574a7bb', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit12c1165ec2a392b21c0fbfeb2574a7bb', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

View File

@@ -0,0 +1,123 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb
{
public static $files = array (
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Cache\\' => 10,
'PhpOption\\' => 10,
),
'G' =>
array (
'GrahamCampbell\\ResultType\\' => 26,
),
'F' =>
array (
'Firebase\\JWT\\' => 13,
'FastRoute\\' => 10,
),
'D' =>
array (
'Dotenv\\' => 7,
'Doctrine\\Deprecations\\' => 22,
'Doctrine\\DBAL\\' => 14,
),
'A' =>
array (
'App\\' => 4,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/cache/src',
),
'PhpOption\\' =>
array (
0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption',
),
'GrahamCampbell\\ResultType\\' =>
array (
0 => __DIR__ . '/..' . '/graham-campbell/result-type/src',
),
'Firebase\\JWT\\' =>
array (
0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
),
'FastRoute\\' =>
array (
0 => __DIR__ . '/..' . '/nikic/fast-route/src',
),
'Dotenv\\' =>
array (
0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src',
),
'Doctrine\\Deprecations\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/deprecations/src',
),
'Doctrine\\DBAL\\' =>
array (
0 => __DIR__ . '/..' . '/doctrine/dbal/src',
),
'App\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit12c1165ec2a392b21c0fbfeb2574a7bb::$classMap;
}, null, ClassLoader::class);
}
}

868
backend/vendor/composer/installed.json vendored Normal file
View File

@@ -0,0 +1,868 @@
{
"packages": [
{
"name": "doctrine/dbal",
"version": "4.2.3",
"version_normalized": "4.2.3.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/33d2d7fe1269b2301640c44cf2896ea607b30e3e",
"reference": "33d2d7fe1269b2301640c44cf2896ea607b30e3e",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^0.5.3|^1",
"php": "^8.1",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.2",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "10.5.39",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^6.3.8|^7.0",
"symfony/console": "^5.4|^6.3|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"time": "2025-03-07T18:29:05+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
"abstraction",
"database",
"db2",
"dbal",
"mariadb",
"mssql",
"mysql",
"oci8",
"oracle",
"pdo",
"pgsql",
"postgresql",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/4.2.3"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
"type": "tidelift"
}
],
"install-path": "../doctrine/dbal"
},
{
"name": "doctrine/deprecations",
"version": "1.1.5",
"version_normalized": "1.1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/deprecations.git",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"phpunit/phpunit": "<=7.5 || >=13"
},
"require-dev": {
"doctrine/coding-standard": "^9 || ^12 || ^13",
"phpstan/phpstan": "1.4.10 || 2.1.11",
"phpstan/phpstan-phpunit": "^1.0 || ^2",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12",
"psr/log": "^1 || ^2 || ^3"
},
"suggest": {
"psr/log": "Allows logging deprecations via PSR-3 logger implementation"
},
"time": "2025-04-07T20:06:18+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
"homepage": "https://www.doctrine-project.org/",
"support": {
"issues": "https://github.com/doctrine/deprecations/issues",
"source": "https://github.com/doctrine/deprecations/tree/1.1.5"
},
"install-path": "../doctrine/deprecations"
},
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"version_normalized": "6.11.1.0",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"time": "2025-04-09T20:32:01+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
"install-path": "../firebase/php-jwt"
},
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"version_normalized": "1.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"time": "2024-07-20T21:45:45+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"install-path": "../graham-campbell/result-type"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
"version_normalized": "1.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/FastRoute.git",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812",
"reference": "181d480e08d9476e61381e04a71b34dc0432e812",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|~5.7"
},
"time": "2018-02-13T20:26:39+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"FastRoute\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Nikita Popov",
"email": "nikic@php.net"
}
],
"description": "Fast request router for PHP",
"keywords": [
"router",
"routing"
],
"support": {
"issues": "https://github.com/nikic/FastRoute/issues",
"source": "https://github.com/nikic/FastRoute/tree/master"
},
"install-path": "../nikic/fast-route"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
"version_normalized": "1.9.3.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"time": "2024-07-20T21:41:07+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"install-path": "../phpoption/phpoption"
},
{
"name": "psr/cache",
"version": "3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"time": "2021-02-03T23:26:27+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
],
"support": {
"source": "https://github.com/php-fig/cache/tree/3.0.0"
},
"install-path": "../psr/cache"
},
{
"name": "psr/log",
"version": "3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"time": "2024-09-11T13:17:53+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"install-path": "../psr/log"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"version_normalized": "1.31.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/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"
}
],
"install-path": "../symfony/polyfill-ctype"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"version_normalized": "1.31.0.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"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"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"
}
],
"install-path": "../symfony/polyfill-mbstring"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.31.0",
"version_normalized": "1.31.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/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"
}
],
"install-path": "../symfony/polyfill-php80"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.1",
"version_normalized": "5.6.1.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"time": "2024-07-20T21:52:34+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"install-path": "../vlucas/phpdotenv"
}
],
"dev": true,
"dev-package-names": []
}

131
backend/vendor/composer/installed.php vendored Normal file
View File

@@ -0,0 +1,131 @@
<?php return array(
'root' => array(
'name' => 'nidoai/backend',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'doctrine/dbal' => array(
'pretty_version' => '4.2.3',
'version' => '4.2.3.0',
'reference' => '33d2d7fe1269b2301640c44cf2896ea607b30e3e',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/dbal',
'aliases' => array(),
'dev_requirement' => false,
),
'doctrine/deprecations' => array(
'pretty_version' => '1.1.5',
'version' => '1.1.5.0',
'reference' => '459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38',
'type' => 'library',
'install_path' => __DIR__ . '/../doctrine/deprecations',
'aliases' => array(),
'dev_requirement' => false,
),
'firebase/php-jwt' => array(
'pretty_version' => 'v6.11.1',
'version' => '6.11.1.0',
'reference' => 'd1e91ecf8c598d073d0995afa8cd5c75c6e19e66',
'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(),
'dev_requirement' => false,
),
'graham-campbell/result-type' => array(
'pretty_version' => 'v1.1.3',
'version' => '1.1.3.0',
'reference' => '3ba905c11371512af9d9bdd27d99b782216b6945',
'type' => 'library',
'install_path' => __DIR__ . '/../graham-campbell/result-type',
'aliases' => array(),
'dev_requirement' => false,
),
'nidoai/backend' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'nikic/fast-route' => array(
'pretty_version' => 'v1.3.0',
'version' => '1.3.0.0',
'reference' => '181d480e08d9476e61381e04a71b34dc0432e812',
'type' => 'library',
'install_path' => __DIR__ . '/../nikic/fast-route',
'aliases' => array(),
'dev_requirement' => false,
),
'phpoption/phpoption' => array(
'pretty_version' => '1.9.3',
'version' => '1.9.3.0',
'reference' => 'e3fac8b24f56113f7cb96af14958c0dd16330f54',
'type' => 'library',
'install_path' => __DIR__ . '/../phpoption/phpoption',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/cache' => array(
'pretty_version' => '3.0.0',
'version' => '3.0.0.0',
'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/cache',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log' => array(
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '85181ba99b2345b0ef10ce42ecac37612d9fd341',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '60328e362d4c2c802a54fcbf04f9d3fb892b4cf8',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
'vlucas/phpdotenv' => array(
'pretty_version' => 'v5.6.1',
'version' => '5.6.1.0',
'reference' => 'a59a13791077fe3d44f90e7133eb68e7d22eaff2',
'type' => 'library',
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,6 @@
This repository has [guidelines specific to testing][testing guidelines], and
Doctrine has [general contributing guidelines][contributor workflow], make
sure you follow both.
[contributor workflow]: https://www.doctrine-project.org/contribute/index.html
[testing guidelines]: https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/testing.html

19
backend/vendor/doctrine/dbal/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2006-2018 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

55
backend/vendor/doctrine/dbal/README.md vendored Normal file
View File

@@ -0,0 +1,55 @@
# Doctrine DBAL
| [5.0-dev][5.0] | [4.3-dev][4.3] | [4.2][4.2] | [3.10][3.10] | [3.9][3.9] |
|:---------------------------------------------------:|:---------------------------------------------------:|:---------------------------------------------------:|:-----------------------------------------------------:|:---------------------------------------------------:|
| [![GitHub Actions][GA 5.0 image]][GA 5.0] | [![GitHub Actions][GA 4.3 image]][GA 4.3] | [![GitHub Actions][GA 4.2 image]][GA 4.2] | [![GitHub Actions][GA 3.10 image]][GA 3.10] | [![GitHub Actions][GA 3.9 image]][GA 3.9] |
| [![AppVeyor][AppVeyor 5.0 image]][AppVeyor 5.0] | [![AppVeyor][AppVeyor 4.3 image]][AppVeyor 4.3] | [![AppVeyor][AppVeyor 4.2 image]][AppVeyor 4.2] | [![AppVeyor][AppVeyor 3.10 image]][AppVeyor 3.10] | [![AppVeyor][AppVeyor 3.9 image]][AppVeyor 3.9] |
| [![Code Coverage][Coverage 5.0 image]][CodeCov 5.0] | [![Code Coverage][Coverage 4.3 image]][CodeCov 4.3] | [![Code Coverage][Coverage 4.2 image]][CodeCov 4.2] | [![Code Coverage][Coverage 3.10 image]][CodeCov 3.10] | [![Code Coverage][Coverage 3.9 image]][CodeCov 3.9] |
Powerful ***D***ata***B***ase ***A***bstraction ***L***ayer with many features for database schema introspection and schema management.
## More resources:
* [Website](http://www.doctrine-project.org/projects/dbal.html)
* [Documentation](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/)
* [Issue Tracker](https://github.com/doctrine/dbal/issues)
[Coverage 5.0 image]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x/graph/badge.svg
[5.0]: https://github.com/doctrine/dbal/tree/5.0.x
[CodeCov 5.0]: https://codecov.io/gh/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0]: https://ci.appveyor.com/project/doctrine/dbal/branch/5.0.x
[AppVeyor 5.0 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/5.0.x?svg=true
[GA 5.0]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A5.0.x
[GA 5.0 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=5.0.x
[Coverage 4.3 image]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x/graph/badge.svg
[4.3]: https://github.com/doctrine/dbal/tree/4.3.x
[CodeCov 4.3]: https://codecov.io/gh/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.3.x
[AppVeyor 4.3 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.3.x?svg=true
[GA 4.3]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.3.x
[GA 4.3 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.3.x
[Coverage 4.2 image]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x/graph/badge.svg
[4.2]: https://github.com/doctrine/dbal/tree/4.2.x
[CodeCov 4.2]: https://codecov.io/gh/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2]: https://ci.appveyor.com/project/doctrine/dbal/branch/4.2.x
[AppVeyor 4.2 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/4.2.x?svg=true
[GA 4.2]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A4.2.x
[GA 4.2 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=4.2.x
[Coverage 3.10 image]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x/graph/badge.svg
[3.10]: https://github.com/doctrine/dbal/tree/3.10.x
[CodeCov 3.10]: https://codecov.io/gh/doctrine/dbal/branch/3.10.x
[AppVeyor 3.10]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.10.x
[AppVeyor 3.10 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.10.x?svg=true
[GA 3.10]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.10.x
[GA 3.10 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.10.x
[Coverage 3.9 image]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x/graph/badge.svg
[3.9]: https://github.com/doctrine/dbal/tree/3.9.x
[CodeCov 3.9]: https://codecov.io/gh/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9]: https://ci.appveyor.com/project/doctrine/dbal/branch/3.9.x
[AppVeyor 3.9 image]: https://ci.appveyor.com/api/projects/status/i88kitq8qpbm0vie/branch/3.9.x?svg=true
[GA 3.9]: https://github.com/doctrine/dbal/actions?query=workflow%3A%22Continuous+Integration%22+branch%3A3.9.x
[GA 3.9 image]: https://github.com/doctrine/dbal/actions/workflows/continuous-integration.yml/badge.svg?branch=3.9.x

View File

@@ -0,0 +1,68 @@
{
"name": "doctrine/dbal",
"type": "library",
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"keywords": [
"abstraction",
"database",
"dbal",
"db2",
"mariadb",
"mssql",
"mysql",
"pgsql",
"postgresql",
"oci8",
"oracle",
"pdo",
"queryobject",
"sasql",
"sql",
"sqlite",
"sqlserver",
"sqlsrv"
],
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"license": "MIT",
"authors": [
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"}
],
"require": {
"php": "^8.1",
"doctrine/deprecations": "^0.5.3|^1",
"psr/cache": "^1|^2|^3",
"psr/log": "^1|^2|^3"
},
"require-dev": {
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.2",
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan-phpunit": "2.0.3",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "10.5.39",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.10.2",
"symfony/cache": "^6.3.8|^7.0",
"symfony/console": "^5.4|^6.3|^7.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true
}
},
"autoload": {
"psr-4": { "Doctrine\\DBAL\\": "src" }
},
"autoload-dev": {
"psr-4": { "Doctrine\\DBAL\\Tests\\": "tests" }
}
}

View File

@@ -0,0 +1,91 @@
parameters:
ignoreErrors:
-
message: '#^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: src/Driver/IBMDB2/Connection.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Connection\:\:exec\(\) never returns numeric\-string so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: src/Driver/OCI8/Connection.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllAssociative\(\) should return list\<array\<string, mixed\>\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Driver/OCI8/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\OCI8\\Result\:\:fetchAllNumeric\(\) should return list\<list\<mixed\>\> but returns array\<mixed\>\.$#'
identifier: return.type
count: 1
path: src/Driver/OCI8/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PDO\\Result\:\:fetchAll\(\) should return list\<mixed\> but returns array\.$#'
identifier: return.type
count: 1
path: src/Driver/PDO/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllAssociative\(\) should return list\<array\<string, mixed\>\> but returns array\<int, array\<string, mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchAllNumeric\(\) should return list\<list\<mixed\>\> but returns array\<int, list\<mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:fetchFirstColumn\(\) should return list\<mixed\> but returns array\<int, bool\|float\|int\|string\|null\>\.$#'
identifier: return.type
count: 1
path: src/Driver/PgSQL/Result.php
-
message: '#^Method Doctrine\\DBAL\\Driver\\SQLite3\\Result\:\:fetchNumeric\(\) should return list\<mixed\>\|false but returns array\|false\.$#'
identifier: return.type
count: 1
path: src/Driver/SQLite3/Result.php
-
message: '#^Template type T is declared as covariant, but occurs in invariant position in property Doctrine\\DBAL\\Schema\\AbstractSchemaManager\:\:\$platform\.$#'
identifier: generics.variance
count: 1
path: src/Schema/AbstractSchemaManager.php
-
message: '#^Loose comparison via "\!\=" is not allowed\.$#'
identifier: notEqual.notAllowed
count: 1
path: src/Schema/ColumnDiff.php
-
message: '#^Method Doctrine\\DBAL\\Schema\\SQLiteSchemaManager\:\:addDetailsToTableForeignKeyColumns\(\) should return list\<array\<string, mixed\>\> but returns array\<int\<0, max\>, array\<string, mixed\>\>\.$#'
identifier: return.type
count: 1
path: src/Schema/SQLiteSchemaManager.php
-
message: '#^Offset string might not exist on array\{application_name\?\: string, charset\?\: string, dbname\?\: string, defaultTableOptions\?\: array\<string, mixed\>, driver\?\: ''ibm_db2''\|''mysqli''\|''oci8''\|''pdo_mysql''\|''pdo_oci''\|''pdo_pgsql''\|''pdo_sqlite''\|''pdo_sqlsrv''\|''pgsql''\|''sqlite3''\|''sqlsrv'', driverClass\?\: class\-string\<Doctrine\\DBAL\\Driver\>, driverOptions\?\: array\<mixed\>, host\?\: string, \.\.\.\}\.$#'
identifier: offsetAccess.notFound
count: 1
path: tests/DriverManagerTest.php
-
message: '#^Call to new Doctrine\\DBAL\\Driver\\PgSQL\\Result\(\) on a separate line has no effect\.$#'
identifier: new.resultUnused
count: 1
path: tests/Functional/Driver/PgSQL/ResultTest.php
-
message: '#^Call to function array_filter\(\) requires parameter \#2 to be passed to avoid loose comparison semantics\.$#'
identifier: arrayFilter.strict
count: 1
path: tests/Functional/Schema/MySQL/JsonCollationTest.php

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
enum ArrayParameterType
{
/**
* Represents an array of ints to be expanded by Doctrine SQL parsing.
*/
case INTEGER;
/**
* Represents an array of strings to be expanded by Doctrine SQL parsing.
*/
case STRING;
/**
* Represents an array of ascii strings to be expanded by Doctrine SQL parsing.
*/
case ASCII;
/**
* Represents an array of ascii strings to be expanded by Doctrine SQL parsing.
*/
case BINARY;
/** @internal */
public static function toElementParameterType(self $type): ParameterType
{
return match ($type) {
self::INTEGER => ParameterType::INTEGER,
self::STRING => ParameterType::STRING,
self::ASCII => ParameterType::ASCII,
self::BINARY => ParameterType::BINARY,
};
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters;
use Throwable;
/** @internal */
interface Exception extends Throwable
{
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters\Exception;
use Doctrine\DBAL\ArrayParameters\Exception;
use LogicException;
use function sprintf;
class MissingNamedParameter extends LogicException implements Exception
{
public static function new(string $name): self
{
return new self(
sprintf('Named parameter "%s" does not have a bound value.', $name),
);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\ArrayParameters\Exception;
use Doctrine\DBAL\ArrayParameters\Exception;
use LogicException;
use function sprintf;
/** @internal */
class MissingPositionalParameter extends LogicException implements Exception
{
public static function new(int $index): self
{
return new self(
sprintf('Positional parameter at index %d does not have a bound value.', $index),
);
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use function array_combine;
use function array_keys;
use function array_map;
use function array_values;
use function count;
/** @internal The class is internal to the caching layer implementation. */
final class ArrayResult implements Result
{
private int $num = 0;
/**
* @param list<string> $columnNames The names of the result columns. Must be non-empty.
* @param list<list<mixed>> $rows The rows of the result. Each row must have the same number of columns
* as the number of column names.
*/
public function __construct(
private readonly array $columnNames,
private array $rows,
) {
}
public function fetchNumeric(): array|false
{
return $this->fetch();
}
public function fetchAssociative(): array|false
{
$row = $this->fetch();
if ($row === false) {
return false;
}
return array_combine($this->columnNames, $row);
}
public function fetchOne(): mixed
{
$row = $this->fetch();
if ($row === false) {
return false;
}
return $row[0];
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return FetchUtils::fetchAllNumeric($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return FetchUtils::fetchAllAssociative($this);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return FetchUtils::fetchFirstColumn($this);
}
public function rowCount(): int
{
return count($this->rows);
}
public function columnCount(): int
{
return count($this->columnNames);
}
public function getColumnName(int $index): string
{
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
}
public function free(): void
{
$this->rows = [];
}
/** @return array{list<string>, list<list<mixed>>} */
public function __serialize(): array
{
return [$this->columnNames, $this->rows];
}
/** @param mixed[] $data */
public function __unserialize(array $data): void
{
// Handle objects serialized with DBAL 4.1 and earlier.
if (isset($data["\0" . self::class . "\0data"])) {
/** @var list<array<string, mixed>> $legacyData */
$legacyData = $data["\0" . self::class . "\0data"];
$this->columnNames = array_keys($legacyData[0] ?? []);
$this->rows = array_map(array_values(...), $legacyData);
return;
}
[$this->columnNames, $this->rows] = $data;
}
/** @return list<mixed>|false */
private function fetch(): array|false
{
if (! isset($this->rows[$this->num])) {
return false;
}
return $this->rows[$this->num++];
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Exception;
class CacheException extends \Exception implements Exception
{
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache\Exception;
use Doctrine\DBAL\Cache\CacheException;
final class NoCacheKey extends CacheException
{
public static function new(): self
{
return new self('No cache key was set.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache\Exception;
use Doctrine\DBAL\Cache\CacheException;
final class NoResultDriverConfigured extends CacheException
{
public static function new(): self
{
return new self('Trying to cache a query but no result driver is configured.');
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Cache;
use Doctrine\DBAL\Cache\Exception\NoCacheKey;
use Doctrine\DBAL\Connection;
use Psr\Cache\CacheItemPoolInterface;
use function hash;
use function serialize;
use function sha1;
/**
* Query Cache Profile handles the data relevant for query caching.
*
* It is a value object, setter methods return NEW instances.
*
* @phpstan-import-type WrapperParameterType from Connection
*/
class QueryCacheProfile
{
public function __construct(
private readonly int $lifetime = 0,
private readonly ?string $cacheKey = null,
private readonly ?CacheItemPoolInterface $resultCache = null,
) {
}
public function getResultCache(): ?CacheItemPoolInterface
{
return $this->resultCache;
}
public function getLifetime(): int
{
return $this->lifetime;
}
/** @throws CacheException */
public function getCacheKey(): string
{
if ($this->cacheKey === null) {
throw NoCacheKey::new();
}
return $this->cacheKey;
}
/**
* Generates the real cache key from query, params, types and connection parameters.
*
* @param list<mixed>|array<string, mixed> $params
* @param array<string, mixed> $connectionParams
* @phpstan-param array<int, WrapperParameterType>|array<string, WrapperParameterType> $types
*
* @return array{string, string}
*/
public function generateCacheKeys(string $sql, array $params, array $types, array $connectionParams = []): array
{
if (isset($connectionParams['password'])) {
unset($connectionParams['password']);
}
$realCacheKey = 'query=' . $sql .
'&params=' . serialize($params) .
'&types=' . serialize($types) .
'&connectionParams=' . hash('sha256', serialize($connectionParams));
// should the key be automatically generated using the inputs or is the cache key set?
$cacheKey = $this->cacheKey ?? sha1($realCacheKey);
return [$cacheKey, $realCacheKey];
}
public function setResultCache(CacheItemPoolInterface $cache): QueryCacheProfile
{
return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache);
}
public function setCacheKey(?string $cacheKey): self
{
return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCache);
}
public function setLifetime(int $lifetime): self
{
return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCache);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
/**
* Contains portable column case conversions.
*/
enum ColumnCase
{
/**
* Convert column names to upper case.
*/
case UPPER;
/**
* Convert column names to lower case.
*/
case LOWER;
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Schema\SchemaManagerFactory;
use Psr\Cache\CacheItemPoolInterface;
/**
* Configuration container for the Doctrine DBAL.
*/
class Configuration
{
/** @var Middleware[] */
private array $middlewares = [];
/**
* The cache driver implementation that is used for query result caching.
*/
private ?CacheItemPoolInterface $resultCache = null;
/**
* The callable to use to filter schema assets.
*
* @var callable
*/
protected $schemaAssetsFilter;
/**
* The default auto-commit mode for connections.
*/
protected bool $autoCommit = true;
private ?SchemaManagerFactory $schemaManagerFactory = null;
public function __construct()
{
$this->schemaAssetsFilter = static function (): bool {
return true;
};
}
/**
* Gets the cache driver implementation that is used for query result caching.
*/
public function getResultCache(): ?CacheItemPoolInterface
{
return $this->resultCache;
}
/**
* Sets the cache driver implementation that is used for query result caching.
*/
public function setResultCache(CacheItemPoolInterface $cache): void
{
$this->resultCache = $cache;
}
/**
* Sets the callable to use to filter schema assets.
*/
public function setSchemaAssetsFilter(callable $schemaAssetsFilter): void
{
$this->schemaAssetsFilter = $schemaAssetsFilter;
}
/**
* Returns the callable to use to filter schema assets.
*/
public function getSchemaAssetsFilter(): callable
{
return $this->schemaAssetsFilter;
}
/**
* Sets the default auto-commit mode for connections.
*
* If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual
* transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either
* the method commit or the method rollback. By default, new connections are in auto-commit mode.
*
* @see getAutoCommit
*
* @param bool $autoCommit True to enable auto-commit mode; false to disable it
*/
public function setAutoCommit(bool $autoCommit): void
{
$this->autoCommit = $autoCommit;
}
/**
* Returns the default auto-commit mode for connections.
*
* @see setAutoCommit
*
* @return bool True if auto-commit mode is enabled by default for connections, false otherwise.
*/
public function getAutoCommit(): bool
{
return $this->autoCommit;
}
/**
* @param Middleware[] $middlewares
*
* @return $this
*/
public function setMiddlewares(array $middlewares): self
{
$this->middlewares = $middlewares;
return $this;
}
/** @return Middleware[] */
public function getMiddlewares(): array
{
return $this->middlewares;
}
public function getSchemaManagerFactory(): ?SchemaManagerFactory
{
return $this->schemaManagerFactory;
}
/** @return $this */
public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFactory): self
{
$this->schemaManagerFactory = $schemaManagerFactory;
return $this;
}
/** @return true */
public function getDisableTypeComments(): bool
{
return true;
}
/**
* @param true $disableTypeComments
*
* @return $this
*/
public function setDisableTypeComments(bool $disableTypeComments): self
{
if (! $disableTypeComments) {
throw new InvalidArgumentException('Column comments cannot be enabled anymore.');
}
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Connection;
use Doctrine\DBAL\ServerVersionProvider;
class StaticServerVersionProvider implements ServerVersionProvider
{
public function __construct(private readonly string $version)
{
}
public function getServerVersion(): string
{
return $this->version;
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
class ConnectionException extends \Exception implements Exception
{
}

View File

@@ -0,0 +1,327 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Connections;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception as DriverException;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Statement;
use InvalidArgumentException;
use SensitiveParameter;
use function array_rand;
use function assert;
use function count;
/**
* Primary-Replica Connection
*
* Connection can be used with primary-replica setups.
*
* Important for the understanding of this connection should be how and when
* it picks the replica or primary.
*
* 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
* or 'executeQuery' is used.
* 2. Primary picked when 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
* 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit' or 'prepare' is called.
* 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
* 4. One replica connection is randomly picked ONCE during a request.
*
* ATTENTION: You can write to the replica with this connection if you execute a write query without
* opening up a transaction. For example:
*
* $conn = DriverManager::getConnection(...);
* $conn->executeQuery("DELETE FROM table");
*
* Be aware that Connection#executeQuery is a method specifically for READ
* operations only.
*
* Use Connection#executeStatement for any SQL statement that changes/updates
* state in the database (UPDATE, INSERT, DELETE or DDL statements).
*
* This connection is limited to replica operations using the
* Connection#executeQuery operation only, because it wouldn't be compatible
* with the ORM or SchemaManager code otherwise. Both use all the other
* operations in a context where writes could happen to a replica, which makes
* this restricted approach necessary.
*
* You can manually connect to the primary at any time by calling:
*
* $conn->ensureConnectedToPrimary();
*
* Instantiation through the DriverManager looks like:
*
* @phpstan-import-type Params from DriverManager
* @phpstan-import-type OverrideParams from DriverManager
* @example
*
* $conn = DriverManager::getConnection(array(
* 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection',
* 'driver' => 'pdo_mysql',
* 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''),
* 'replica' => array(
* array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''),
* array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''),
* )
* ));
*
* You can also pass 'driverOptions' and any other documented option to each of this drivers
* to pass additional information.
*/
class PrimaryReadReplicaConnection extends Connection
{
/**
* Primary and Replica connection (one of the randomly picked replicas).
*
* @var array<string, DriverConnection|null>
*/
protected array $connections = ['primary' => null, 'replica' => null];
/**
* You can keep the replica connection and then switch back to it
* during the request if you know what you are doing.
*/
protected bool $keepReplica = false;
/**
* Creates Primary Replica Connection.
*
* @internal The connection can be only instantiated by the driver manager.
*
* @param array<string, mixed> $params
* @phpstan-param Params $params
*/
public function __construct(array $params, Driver $driver, ?Configuration $config = null)
{
if (! isset($params['replica'], $params['primary'])) {
throw new InvalidArgumentException('primary or replica configuration missing');
}
if (count($params['replica']) === 0) {
throw new InvalidArgumentException('You have to configure at least one replica.');
}
if (isset($params['driver'])) {
$params['primary']['driver'] = $params['driver'];
foreach ($params['replica'] as $replicaKey => $replica) {
$params['replica'][$replicaKey]['driver'] = $params['driver'];
}
}
$this->keepReplica = ! empty($params['keepReplica']);
parent::__construct($params, $driver, $config);
}
/**
* Checks if the connection is currently towards the primary or not.
*/
public function isConnectedToPrimary(): bool
{
return $this->_conn !== null && $this->_conn === $this->connections['primary'];
}
public function connect(?string $connectionName = null): DriverConnection
{
if ($connectionName !== null) {
throw new InvalidArgumentException(
'Passing a connection name as first argument is not supported anymore.'
. ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.',
);
}
return $this->performConnect();
}
protected function performConnect(?string $connectionName = null): DriverConnection
{
$requestedConnectionChange = ($connectionName !== null);
$connectionName ??= 'replica';
if ($connectionName !== 'replica' && $connectionName !== 'primary') {
throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.');
}
// If we have a connection open, and this is not an explicit connection
// change request, then abort right here, because we are already done.
// This prevents writes to the replica in case of "keepReplica" option enabled.
if ($this->_conn !== null && ! $requestedConnectionChange) {
return $this->_conn;
}
$forcePrimaryAsReplica = false;
if ($this->getTransactionNestingLevel() > 0) {
$connectionName = 'primary';
$forcePrimaryAsReplica = true;
}
if (isset($this->connections[$connectionName])) {
$this->_conn = $this->connections[$connectionName];
if ($forcePrimaryAsReplica && ! $this->keepReplica) {
$this->connections['replica'] = $this->_conn;
}
return $this->_conn;
}
if ($connectionName === 'primary') {
$this->connections['primary'] = $this->_conn = $this->connectTo($connectionName);
// Set replica connection to primary to avoid invalid reads
if (! $this->keepReplica) {
$this->connections['replica'] = $this->connections['primary'];
}
} else {
$this->connections['replica'] = $this->_conn = $this->connectTo($connectionName);
}
return $this->_conn;
}
/**
* Connects to the primary node of the database cluster.
*
* All following statements after this will be executed against the primary node.
*/
public function ensureConnectedToPrimary(): void
{
$this->performConnect('primary');
}
/**
* Connects to a replica node of the database cluster.
*
* All following statements after this will be executed against the replica node,
* unless the keepReplica option is set to false and a primary connection
* was already opened.
*/
public function ensureConnectedToReplica(): void
{
$this->performConnect('replica');
}
/**
* Connects to a specific connection.
*
* @throws Exception
*/
protected function connectTo(string $connectionName): DriverConnection
{
$params = $this->getParams();
assert(isset($params['primary']));
if ($connectionName === 'primary') {
$connectionParams = $params['primary'];
} else {
assert(isset($params['replica']));
$connectionParams = $this->chooseReplicaConnectionParameters($params['primary'], $params['replica']);
}
try {
return $this->driver->connect($connectionParams);
} catch (DriverException $e) {
throw $this->convertException($e);
}
}
/**
* @param OverrideParams $primary
* @param array<OverrideParams> $replicas
*
* @return array<string, mixed>
* @phpstan-return OverrideParams
*/
protected function chooseReplicaConnectionParameters(
#[SensitiveParameter]
array $primary,
#[SensitiveParameter]
array $replicas,
): array {
$params = $replicas[array_rand($replicas)];
if (! isset($params['charset']) && isset($primary['charset'])) {
$params['charset'] = $primary['charset'];
}
return $params;
}
/**
* {@inheritDoc}
*/
public function executeStatement(string $sql, array $params = [], array $types = []): int|string
{
$this->ensureConnectedToPrimary();
return parent::executeStatement($sql, $params, $types);
}
public function beginTransaction(): void
{
$this->ensureConnectedToPrimary();
parent::beginTransaction();
}
public function commit(): void
{
$this->ensureConnectedToPrimary();
parent::commit();
}
public function rollBack(): void
{
$this->ensureConnectedToPrimary();
parent::rollBack();
}
public function close(): void
{
unset($this->connections['primary'], $this->connections['replica']);
parent::close();
$this->_conn = null;
$this->connections = ['primary' => null, 'replica' => null];
}
public function createSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::createSavepoint($savepoint);
}
public function releaseSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::releaseSavepoint($savepoint);
}
public function rollbackSavepoint(string $savepoint): void
{
$this->ensureConnectedToPrimary();
parent::rollbackSavepoint($savepoint);
}
public function prepare(string $sql): Statement
{
$this->ensureConnectedToPrimary();
return parent::prepare($sql);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use SensitiveParameter;
/**
* Driver interface.
* Interface that all DBAL drivers must implement.
*
* @phpstan-import-type Params from DriverManager
*/
interface Driver
{
/**
* Attempts to create a connection with the database.
*
* @param array<string, mixed> $params All connection parameters.
* @phpstan-param Params $params All connection parameters.
*
* @return DriverConnection The database connection.
*
* @throws Exception
*/
public function connect(
#[SensitiveParameter]
array $params,
): DriverConnection;
/**
* Gets the DatabasePlatform instance that provides all the metadata about
* the platform this driver connects to.
*
* @return AbstractPlatform The database platform.
*/
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform;
/**
* Gets the ExceptionConverter that can be used to convert driver-level exceptions into DBAL exceptions.
*/
public function getExceptionConverter(): ExceptionConverter;
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Query;
interface ExceptionConverter
{
/**
* Converts a given driver-level exception into a DBAL-level driver exception.
*
* Implementors should use the vendor-specific error code and SQLSTATE of the exception
* and instantiate the most appropriate specialized {@see DriverException} subclass.
*
* @param Exception $exception The driver exception to convert.
* @param Query|null $query The SQL query that triggered the exception, if any.
*
* @return DriverException An instance of {@see DriverException} or one of its subclasses.
*/
public function convert(Exception $exception, ?Query $query): DriverException;
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\IBMDB2;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/**
* @internal
*
* @link https://www.ibm.com/docs/en/db2/11.5?topic=messages-sql
*/
final class ExceptionConverter implements ExceptionConverterInterface
{
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
-104 => new SyntaxErrorException($exception, $query),
-203 => new NonUniqueFieldNameException($exception, $query),
-204 => new TableNotFoundException($exception, $query),
-206 => new InvalidFieldNameException($exception, $query),
-407 => new NotNullConstraintViolationException($exception, $query),
-530,
-531,
-532,
-20356 => new ForeignKeyConstraintViolationException($exception, $query),
-601 => new TableExistsException($exception, $query),
-803 => new UniqueConstraintViolationException($exception, $query),
-1336,
-30082 => new ConnectionException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\MySQL;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/**
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
* @link https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
*/
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
1008 => new DatabaseDoesNotExist($exception, $query),
1213 => new DeadlockException($exception, $query),
1205 => new LockWaitTimeoutException($exception, $query),
1050 => new TableExistsException($exception, $query),
1051,
1146 => new TableNotFoundException($exception, $query),
1216,
1217,
1451,
1452,
1701 => new ForeignKeyConstraintViolationException($exception, $query),
1062,
1557,
1569,
1586 => new UniqueConstraintViolationException($exception, $query),
1054,
1166,
1611 => new InvalidFieldNameException($exception, $query),
1052,
1060,
1110 => new NonUniqueFieldNameException($exception, $query),
1064,
1149,
1287,
1341,
1342,
1343,
1344,
1382,
1479,
1541,
1554,
1626 => new SyntaxErrorException($exception, $query),
1044,
1045,
1046,
1049,
1095,
1142,
1143,
1227,
1370,
1429,
2002,
2005,
2054 => new ConnectionException($exception, $query),
2006,
4031 => new ConnectionLost($exception, $query),
1048,
1121,
1138,
1171,
1252,
1263,
1364,
1566 => new NotNullConstraintViolationException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\OCI;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\PDO\Exception as DriverPDOException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\TransactionRolledBack;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function assert;
use function count;
use function explode;
use function str_replace;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.dba-oracle.com/t_error_code_list.htm */
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
1,
2299,
38911 => new UniqueConstraintViolationException($exception, $query),
904 => new InvalidFieldNameException($exception, $query),
918,
960 => new NonUniqueFieldNameException($exception, $query),
923 => new SyntaxErrorException($exception, $query),
942 => new TableNotFoundException($exception, $query),
955 => new TableExistsException($exception, $query),
1017,
12545 => new ConnectionException($exception, $query),
1400 => new NotNullConstraintViolationException($exception, $query),
1918 => new DatabaseDoesNotExist($exception, $query),
2091 => (function () use ($exception, $query) {
//SQLSTATE[HY000]: General error: 2091 OCITransCommit: ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
$lines = explode("\n", $exception->getMessage(), 2);
assert(count($lines) >= 2);
[, $causeError] = $lines;
[$causeCode] = explode(': ', $causeError, 2);
$code = (int) str_replace('ORA-', '', $causeCode);
$sqlState = $exception->getSQLState();
if ($exception instanceof DriverPDOException) {
$why = $this->convert(new DriverPDOException($causeError, $sqlState, $code, $exception), $query);
} else {
$why = $this->convert(new Error($causeError, $sqlState, $code, $exception), $query);
}
return new TransactionRolledBack($why, $query);
})(),
2289,
2443,
4080 => new DatabaseObjectNotFoundException($exception, $query),
2266,
2291,
2292 => new ForeignKeyConstraintViolationException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\PostgreSQL;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SchemaDoesNotExist;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function str_contains;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.postgresql.org/docs/9.4/static/errcodes-appendix.html */
public function convert(Exception $exception, ?Query $query): DriverException
{
switch ($exception->getSQLState()) {
case '40001':
case '40P01':
return new DeadlockException($exception, $query);
case '0A000':
// Foreign key constraint violations during a TRUNCATE operation
// are considered "feature not supported" in PostgreSQL.
if (str_contains($exception->getMessage(), 'truncate')) {
return new ForeignKeyConstraintViolationException($exception, $query);
}
break;
case '23502':
return new NotNullConstraintViolationException($exception, $query);
case '23503':
return new ForeignKeyConstraintViolationException($exception, $query);
case '23505':
return new UniqueConstraintViolationException($exception, $query);
case '3D000':
return new DatabaseDoesNotExist($exception, $query);
case '3F000':
return new SchemaDoesNotExist($exception, $query);
case '42601':
return new SyntaxErrorException($exception, $query);
case '42702':
return new NonUniqueFieldNameException($exception, $query);
case '42703':
return new InvalidFieldNameException($exception, $query);
case '42P01':
return new TableNotFoundException($exception, $query);
case '42P07':
return new TableExistsException($exception, $query);
case '08006':
return new ConnectionException($exception, $query);
}
if (str_contains($exception->getMessage(), 'terminating connection')) {
return new ConnectionLost($exception, $query);
}
return new DriverException($exception, $query);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\SQLSrv;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
/**
* @internal
*
* @link https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors
*/
final class ExceptionConverter implements ExceptionConverterInterface
{
public function convert(Exception $exception, ?Query $query): DriverException
{
return match ($exception->getCode()) {
102 => new SyntaxErrorException($exception, $query),
207 => new InvalidFieldNameException($exception, $query),
208 => new TableNotFoundException($exception, $query),
209 => new NonUniqueFieldNameException($exception, $query),
515 => new NotNullConstraintViolationException($exception, $query),
547,
4712 => new ForeignKeyConstraintViolationException($exception, $query),
2601,
2627 => new UniqueConstraintViolationException($exception, $query),
2714 => new TableExistsException($exception, $query),
3701,
15151 => new DatabaseObjectNotFoundException($exception, $query),
11001,
18456 => new ConnectionException($exception, $query),
default => new DriverException($exception, $query),
};
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\API\SQLite;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidFieldNameException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
use Doctrine\DBAL\Exception\ReadOnlyException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;
use function str_contains;
/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.sqlite.org/c3ref/c_abort.html */
public function convert(Exception $exception, ?Query $query): DriverException
{
if (str_contains($exception->getMessage(), 'database is locked')) {
return new LockWaitTimeoutException($exception, $query);
}
if (
str_contains($exception->getMessage(), 'must be unique') ||
str_contains($exception->getMessage(), 'is not unique') ||
str_contains($exception->getMessage(), 'are not unique') ||
str_contains($exception->getMessage(), 'UNIQUE constraint failed')
) {
return new UniqueConstraintViolationException($exception, $query);
}
if (
str_contains($exception->getMessage(), 'may not be NULL') ||
str_contains($exception->getMessage(), 'NOT NULL constraint failed')
) {
return new NotNullConstraintViolationException($exception, $query);
}
if (str_contains($exception->getMessage(), 'no such table:')) {
return new TableNotFoundException($exception, $query);
}
if (str_contains($exception->getMessage(), 'already exists')) {
return new TableExistsException($exception, $query);
}
if (str_contains($exception->getMessage(), 'has no column named')) {
return new InvalidFieldNameException($exception, $query);
}
if (str_contains($exception->getMessage(), 'ambiguous column name')) {
return new NonUniqueFieldNameException($exception, $query);
}
if (str_contains($exception->getMessage(), 'syntax error')) {
return new SyntaxErrorException($exception, $query);
}
if (str_contains($exception->getMessage(), 'attempt to write a readonly database')) {
return new ReadOnlyException($exception, $query);
}
if (str_contains($exception->getMessage(), 'unable to open database file')) {
return new ConnectionException($exception, $query);
}
if (str_contains($exception->getMessage(), 'FOREIGN KEY constraint failed')) {
return new ForeignKeyConstraintViolationException($exception, $query);
}
return new DriverException($exception, $query);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\IBMDB2\ExceptionConverter;
use Doctrine\DBAL\Platforms\DB2Platform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for IBM DB2 based drivers.
*/
abstract class AbstractDB2Driver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): DB2Platform
{
return new DB2Platform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Exception as BaseException;
use Throwable;
/**
* Abstract base implementation of the {@see DriverException} interface.
*/
abstract class AbstractException extends BaseException implements Exception
{
/**
* @param string $message The driver error message.
* @param string|null $sqlState The SQLSTATE the driver is in at the time the error occurred, if any.
* @param int $code The driver specific error code if any.
* @param Throwable|null $previous The previous throwable used for the exception chaining.
*/
public function __construct(
string $message,
private readonly ?string $sqlState = null,
int $code = 0,
?Throwable $previous = null,
) {
parent::__construct($message, $code, $previous);
}
public function getSQLState(): ?string
{
return $this->sqlState;
}
}

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
use Doctrine\DBAL\Platforms\MariaDB1010Platform;
use Doctrine\DBAL\Platforms\MariaDB1052Platform;
use Doctrine\DBAL\Platforms\MariaDB1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQL84Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\Deprecations\Deprecation;
use function preg_match;
use function stripos;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for MySQL based drivers.
*/
abstract class AbstractMySQLDriver implements Driver
{
/**
* {@inheritDoc}
*
* @throws InvalidPlatformVersion
*/
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractMySQLPlatform
{
$version = $versionProvider->getServerVersion();
if (stripos($version, 'mariadb') !== false) {
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
if (version_compare($mariaDbVersion, '10.10.0', '>=')) {
return new MariaDB1010Platform();
}
if (version_compare($mariaDbVersion, '10.6.0', '>=')) {
return new MariaDB1060Platform();
}
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
return new MariaDB1052Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6343',
'Support for MariaDB < 10.5.2 is deprecated and will be removed in DBAL 5',
);
return new MariaDBPlatform();
}
if (version_compare($version, '8.4.0', '>=')) {
return new MySQL84Platform();
}
if (version_compare($version, '8.0.0', '>=')) {
return new MySQL80Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6343',
'Support for MySQL < 8 is deprecated and will be removed in DBAL 5',
);
return new MySQLPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
/**
* Detect MariaDB server version, including hack for some mariadb distributions
* that starts with the prefix '5.5.5-'
*
* @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial'
*
* @throws InvalidPlatformVersion
*/
private function getMariaDbMysqlVersionNumber(string $versionString): string
{
if (
preg_match(
'/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i',
$versionString,
$versionParts,
) !== 1
) {
throw InvalidPlatformVersion::new(
$versionString,
'^(?:5\.5\.5-)?(mariadb-)?<major_version>.<minor_version>.<patch_version>',
);
}
return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\AbstractOracleDriver\EasyConnectString;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\OCI\ExceptionConverter;
use Doctrine\DBAL\Platforms\OraclePlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for Oracle based drivers.
*/
abstract class AbstractOracleDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): OraclePlatform
{
return new OraclePlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
/**
* Returns an appropriate Easy Connect String for the given parameters.
*
* @param array<string, mixed> $params The connection parameters to return the Easy Connect String for.
*/
protected function getEasyConnectString(array $params): string
{
return (string) EasyConnectString::fromConnectionParameters($params);
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractOracleDriver;
use function implode;
use function is_array;
use function sprintf;
/**
* Represents an Oracle Easy Connect string
*
* @link https://docs.oracle.com/database/121/NETAG/naming.htm
*/
final class EasyConnectString
{
private function __construct(private readonly string $string)
{
}
public function __toString(): string
{
return $this->string;
}
/**
* Creates the object from an array representation
*
* @param mixed[] $params
*/
public static function fromArray(array $params): self
{
return new self(self::renderParams($params));
}
/**
* Creates the object from the given DBAL connection parameters.
*
* @param mixed[] $params
*/
public static function fromConnectionParameters(array $params): self
{
if (isset($params['connectstring'])) {
return new self($params['connectstring']);
}
if (! isset($params['host'])) {
return new self($params['dbname'] ?? '');
}
$connectData = [];
if (isset($params['servicename']) || isset($params['dbname'])) {
$serviceKey = 'SID';
if (isset($params['service'])) {
$serviceKey = 'SERVICE_NAME';
}
$serviceName = $params['servicename'] ?? $params['dbname'];
$connectData[$serviceKey] = $serviceName;
}
if (isset($params['instancename'])) {
$connectData['INSTANCE_NAME'] = $params['instancename'];
}
if (! empty($params['pooled'])) {
$connectData['SERVER'] = 'POOLED';
}
return self::fromArray([
'DESCRIPTION' => [
'ADDRESS' => [
'PROTOCOL' => $params['driverOptions']['protocol'] ?? 'TCP',
'HOST' => $params['host'],
'PORT' => $params['port'] ?? 1521,
],
'CONNECT_DATA' => $connectData,
],
]);
}
/** @param mixed[] $params */
private static function renderParams(array $params): string
{
$chunks = [];
foreach ($params as $key => $value) {
$string = self::renderValue($value);
if ($string === '') {
continue;
}
$chunks[] = sprintf('(%s=%s)', $key, $string);
}
return implode('', $chunks);
}
private static function renderValue(mixed $value): string
{
if (is_array($value)) {
return self::renderParams($value);
}
return (string) $value;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\PostgreSQL\ExceptionConverter;
use Doctrine\DBAL\Platforms\Exception\InvalidPlatformVersion;
use Doctrine\DBAL\Platforms\PostgreSQL120Platform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\Deprecations\Deprecation;
use function preg_match;
use function version_compare;
/**
* Abstract base implementation of the {@see Driver} interface for PostgreSQL based drivers.
*/
abstract class AbstractPostgreSQLDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): PostgreSQLPlatform
{
$version = $versionProvider->getServerVersion();
if (preg_match('/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/', $version, $versionParts) !== 1) {
throw InvalidPlatformVersion::new(
$version,
'<major_version>.<minor_version>.<patch_version>',
);
}
$majorVersion = $versionParts['major'];
$minorVersion = $versionParts['minor'] ?? 0;
$patchVersion = $versionParts['patch'] ?? 0;
$version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion;
if (version_compare($version, '12.0', '>=')) {
return new PostgreSQL120Platform();
}
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6495',
'Support for Postgres < 12 is deprecated and will be removed in DBAL 5',
);
return new PostgreSQLPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\SQLSrv\ExceptionConverter;
use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for Microsoft SQL Server based drivers.
*/
abstract class AbstractSQLServerDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): SQLServerPlatform
{
return new SQLServerPlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class PortWithoutHost extends AbstractException
{
public static function new(): self
{
return new self('Connection port specified without the host');
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\API\SQLite\ExceptionConverter;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Abstract base implementation of the {@see Driver} interface for SQLite based drivers.
*/
abstract class AbstractSQLiteDriver implements Driver
{
public function getDatabasePlatform(ServerVersionProvider $versionProvider): SQLitePlatform
{
return new SQLitePlatform();
}
public function getExceptionConverter(): ExceptionConverterInterface
{
return new ExceptionConverter();
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use SensitiveParameter;
final class EnableForeignKeys implements Middleware
{
public function wrap(Driver $driver): Driver
{
return new class ($driver) extends AbstractDriverMiddleware {
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$connection = parent::connect($params);
$connection->exec('PRAGMA foreign_keys=ON');
return $connection;
}
};
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\ServerVersionProvider;
/**
* Connection interface.
* Driver connections must implement this interface.
*/
interface Connection extends ServerVersionProvider
{
/**
* Prepares a statement for execution and returns a Statement object.
*
* @throws Exception
*/
public function prepare(string $sql): Statement;
/**
* Executes an SQL statement, returning a result set as a Statement object.
*
* @throws Exception
*/
public function query(string $sql): Result;
/**
* Quotes a string for use in a query.
*
* The usage of this method is discouraged. Use prepared statements
* or {@see AbstractPlatform::quoteStringLiteral()} instead.
*/
public function quote(string $value): string;
/**
* Executes an SQL statement and return the number of affected rows.
* If the number of affected rows is greater than the maximum int value (PHP_INT_MAX),
* the number of affected rows may be returned as a string.
*
* @return int|numeric-string
*
* @throws Exception
*/
public function exec(string $sql): int|string;
/**
* Returns the ID of the last inserted row.
*
* This method returns an integer or a string representing the value of the auto-increment column
* from the last row inserted into the database, if any, or throws an exception if a value cannot be returned,
* in particular when:
*
* - the driver does not support identity columns;
* - the last statement dit not return an identity (caution: see note below).
*
* Note: if the last statement was not an INSERT to an autoincrement column, this method MAY return an ID from a
* previous statement. DO NOT RELY ON THIS BEHAVIOR which is driver-dependent: always call this method right after
* executing an INSERT statement.
*
* @throws Exception
*/
public function lastInsertId(): int|string;
/**
* Initiates a transaction.
*
* @throws Exception
*/
public function beginTransaction(): void;
/**
* Commits a transaction.
*
* @throws Exception
*/
public function commit(): void;
/**
* Rolls back the current transaction, as initiated by beginTransaction().
*
* @throws Exception
*/
public function rollBack(): void;
/**
* Provides access to the native database connection.
*
* @return resource|object
*/
public function getNativeConnection();
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Throwable;
/**
* Contract for a driver exception.
*
* Driver exceptions provide the SQLSTATE of the driver
* and the driver specific error code at the time the error occurred.
*/
interface Exception extends Throwable
{
/**
* Returns the SQLSTATE the driver was in at the time the error occurred.
*
* Returns null if the driver does not provide a SQLSTATE for the error occurred.
*/
public function getSQLState(): ?string;
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use Throwable;
/** @internal */
final class IdentityColumnsNotSupported extends AbstractException
{
public static function new(?Throwable $previous = null): self
{
return new self('The driver does not support identity columns.', null, 0, $previous);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use Throwable;
/** @internal */
final class NoIdentityValue extends AbstractException
{
public static function new(?Throwable $previous = null): self
{
return new self('No identity value was generated by the last statement.', null, 0, $previous);
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
/** @internal */
final class FetchUtils
{
/** @throws Exception */
public static function fetchOne(Result $result): mixed
{
$row = $result->fetchNumeric();
if ($row === false) {
return false;
}
return $row[0];
}
/**
* @return list<list<mixed>>
*
* @throws Exception
*/
public static function fetchAllNumeric(Result $result): array
{
$rows = [];
while (($row = $result->fetchNumeric()) !== false) {
$rows[] = $row;
}
return $rows;
}
/**
* @return list<array<string,mixed>>
*
* @throws Exception
*/
public static function fetchAllAssociative(Result $result): array
{
$rows = [];
while (($row = $result->fetchAssociative()) !== false) {
$rows[] = $row;
}
return $rows;
}
/**
* @return list<mixed>
*
* @throws Exception
*/
public static function fetchFirstColumn(Result $result): array
{
$rows = [];
while (($row = $result->fetchOne()) !== false) {
$rows[] = $row;
}
return $rows;
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception\NoIdentityValue;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionError;
use Doctrine\DBAL\Driver\IBMDB2\Exception\PrepareFailed;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use stdClass;
use function assert;
use function db2_autocommit;
use function db2_commit;
use function db2_escape_string;
use function db2_exec;
use function db2_last_insert_id;
use function db2_num_rows;
use function db2_prepare;
use function db2_rollback;
use function db2_server_info;
use function error_get_last;
use const DB2_AUTOCOMMIT_OFF;
use const DB2_AUTOCOMMIT_ON;
final class Connection implements ConnectionInterface
{
/**
* @internal The connection can be only instantiated by its driver.
*
* @param resource $connection
*/
public function __construct(private readonly mixed $connection)
{
}
public function getServerVersion(): string
{
$serverInfo = db2_server_info($this->connection);
assert($serverInfo instanceof stdClass);
return $serverInfo->DBMS_VER;
}
public function prepare(string $sql): Statement
{
$stmt = @db2_prepare($this->connection, $sql);
if ($stmt === false) {
throw PrepareFailed::new(error_get_last());
}
return new Statement($stmt);
}
public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}
public function quote(string $value): string
{
return "'" . db2_escape_string($value) . "'";
}
public function exec(string $sql): int|string
{
$stmt = @db2_exec($this->connection, $sql);
if ($stmt === false) {
throw StatementError::new();
}
$numRows = db2_num_rows($stmt);
if ($numRows === false) {
throw StatementError::new();
}
return $numRows;
}
public function lastInsertId(): string
{
$lastInsertId = db2_last_insert_id($this->connection);
if ($lastInsertId === null) {
throw NoIdentityValue::new();
}
return $lastInsertId;
}
public function beginTransaction(): void
{
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF) !== true) {
throw ConnectionError::new($this->connection);
}
}
public function commit(): void
{
if (! db2_commit($this->connection)) {
throw ConnectionError::new($this->connection);
}
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) {
throw ConnectionError::new($this->connection);
}
}
public function rollBack(): void
{
if (! db2_rollback($this->connection)) {
throw ConnectionError::new($this->connection);
}
if (db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON) !== true) {
throw ConnectionError::new($this->connection);
}
}
/** @return resource */
public function getNativeConnection()
{
return $this->connection;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use SensitiveParameter;
use function implode;
use function sprintf;
use function str_contains;
/**
* IBM DB2 DSN
*/
final class DataSourceName
{
private function __construct(
#[SensitiveParameter]
private readonly string $string,
) {
}
public function toString(): string
{
return $this->string;
}
/**
* Creates the object from an array representation
*
* @param array<string,mixed> $params
*/
public static function fromArray(
#[SensitiveParameter]
array $params,
): self {
$chunks = [];
foreach ($params as $key => $value) {
$chunks[] = sprintf('%s=%s', $key, $value);
}
return new self(implode(';', $chunks));
}
/**
* Creates the object from the given DBAL connection parameters.
*
* @param array<string,mixed> $params
*/
public static function fromConnectionParameters(#[SensitiveParameter]
array $params,): self
{
if (isset($params['dbname']) && str_contains($params['dbname'], '=')) {
return new self($params['dbname']);
}
$dsnParams = [];
foreach (
[
'host' => 'HOSTNAME',
'port' => 'PORT',
'protocol' => 'PROTOCOL',
'dbname' => 'DATABASE',
'user' => 'UID',
'password' => 'PWD',
] as $dbalParam => $dsnParam
) {
if (! isset($params[$dbalParam])) {
continue;
}
$dsnParams[$dsnParam] = $params[$dbalParam];
}
return self::fromArray($dsnParams);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\AbstractDB2Driver;
use Doctrine\DBAL\Driver\IBMDB2\Exception\ConnectionFailed;
use SensitiveParameter;
use function db2_connect;
use function db2_pconnect;
final class Driver extends AbstractDB2Driver
{
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
$dataSourceName = DataSourceName::fromConnectionParameters($params)->toString();
$username = $params['user'] ?? '';
$password = $params['password'] ?? '';
$driverOptions = $params['driverOptions'] ?? [];
if (! empty($params['persistent'])) {
$connection = db2_pconnect($dataSourceName, $username, $password, $driverOptions);
} else {
$connection = db2_connect($dataSourceName, $username, $password, $driverOptions);
}
if ($connection === false) {
throw ConnectionFailed::new();
}
return new Connection($connection);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class CannotCopyStreamToStream extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
$message = 'Could not copy source stream to temporary file';
if ($error !== null) {
$message .= ': ' . $error['message'];
}
return new self($message);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class CannotCreateTemporaryFile extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
$message = 'Could not create temporary file';
if ($error !== null) {
$message .= ': ' . $error['message'];
}
return new self($message);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_conn_error;
use function db2_conn_errormsg;
/** @internal */
final class ConnectionError extends AbstractException
{
/** @param resource $connection */
public static function new($connection): self
{
$message = db2_conn_errormsg($connection);
$sqlState = db2_conn_error($connection);
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_conn_error;
use function db2_conn_errormsg;
/** @internal */
final class ConnectionFailed extends AbstractException
{
public static function new(): self
{
$message = db2_conn_errormsg();
$sqlState = db2_conn_error();
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function preg_match;
/** @internal */
final class Factory
{
/**
* @param callable(int): T $constructor
*
* @return T
*
* @template T of AbstractException
*/
public static function create(string $message, callable $constructor): AbstractException
{
$code = 0;
if (preg_match('/ SQL(\d+)N /', $message, $matches) === 1) {
$code = -(int) $matches[1];
}
return $constructor($code);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class PrepareFailed extends AbstractException
{
/** @phpstan-param array{message: string, ...}|null $error */
public static function new(?array $error): self
{
if ($error === null) {
return new self('Unknown error');
}
return new self($error['message']);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function db2_stmt_error;
use function db2_stmt_errormsg;
/** @internal */
final class StatementError extends AbstractException
{
/** @param resource|null $statement */
public static function new($statement = null): self
{
if ($statement !== null) {
$message = db2_stmt_errormsg($statement);
$sqlState = db2_stmt_error($statement);
} else {
$message = db2_stmt_errormsg();
$sqlState = db2_stmt_error();
}
return Factory::create($message, static function (int $code) use ($message, $sqlState): self {
return new self($message, $sqlState, $code);
});
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\FetchUtils;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use function db2_fetch_array;
use function db2_fetch_assoc;
use function db2_free_result;
use function db2_num_fields;
use function db2_num_rows;
use function db2_stmt_error;
final class Result implements ResultInterface
{
/**
* @internal The result can be only instantiated by its driver connection or statement.
*
* @param resource $statement
*/
public function __construct(private readonly mixed $statement)
{
}
public function fetchNumeric(): array|false
{
$row = @db2_fetch_array($this->statement);
if ($row === false && db2_stmt_error($this->statement) !== '02000') {
throw StatementError::new($this->statement);
}
return $row;
}
public function fetchAssociative(): array|false
{
$row = @db2_fetch_assoc($this->statement);
if ($row === false && db2_stmt_error($this->statement) !== '02000') {
throw StatementError::new($this->statement);
}
return $row;
}
public function fetchOne(): mixed
{
return FetchUtils::fetchOne($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return FetchUtils::fetchAllNumeric($this);
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return FetchUtils::fetchAllAssociative($this);
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return FetchUtils::fetchFirstColumn($this);
}
public function rowCount(): int
{
$numRows = @db2_num_rows($this->statement);
if ($numRows === false) {
throw StatementError::new($this->statement);
}
return $numRows;
}
public function columnCount(): int
{
$count = db2_num_fields($this->statement);
if ($count !== false) {
return $count;
}
return 0;
}
public function getColumnName(int $index): string
{
$name = db2_field_name($this->statement, $index);
if ($name === false) {
throw InvalidColumnIndex::new($index);
}
return $name;
}
public function free(): void
{
db2_free_result($this->statement);
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\IBMDB2;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCopyStreamToStream;
use Doctrine\DBAL\Driver\IBMDB2\Exception\CannotCreateTemporaryFile;
use Doctrine\DBAL\Driver\IBMDB2\Exception\StatementError;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use function assert;
use function db2_bind_param;
use function db2_execute;
use function error_get_last;
use function fclose;
use function is_int;
use function is_resource;
use function stream_copy_to_stream;
use function stream_get_meta_data;
use function tmpfile;
use const DB2_BINARY;
use const DB2_CHAR;
use const DB2_LONG;
use const DB2_PARAM_FILE;
use const DB2_PARAM_IN;
final class Statement implements StatementInterface
{
/** @var mixed[] */
private array $parameters = [];
/**
* Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement
* and the temporary file handle bound to the underlying statement
*
* @var array<int,string|resource|null>
*/
private array $lobs = [];
/**
* @internal The statement can be only instantiated by its driver connection.
*
* @param resource $stmt
*/
public function __construct(private readonly mixed $stmt)
{
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
assert(is_int($param));
switch ($type) {
case ParameterType::INTEGER:
$this->bind($param, $value, DB2_PARAM_IN, DB2_LONG);
break;
case ParameterType::LARGE_OBJECT:
$this->lobs[$param] = &$value;
break;
default:
$this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR);
break;
}
}
/** @throws Exception */
private function bind(int $position, mixed &$variable, int $parameterType, int $dataType): void
{
$this->parameters[$position] =& $variable;
if (! db2_bind_param($this->stmt, $position, '', $parameterType, $dataType)) {
throw StatementError::new($this->stmt);
}
}
public function execute(): Result
{
$handles = $this->bindLobs();
$result = @db2_execute($this->stmt, $this->parameters);
foreach ($handles as $handle) {
fclose($handle);
}
$this->lobs = [];
if ($result === false) {
throw StatementError::new($this->stmt);
}
return new Result($this->stmt);
}
/**
* @return list<resource>
*
* @throws Exception
*/
private function bindLobs(): array
{
$handles = [];
foreach ($this->lobs as $param => $value) {
if (is_resource($value)) {
$handle = $handles[] = $this->createTemporaryFile();
$path = stream_get_meta_data($handle)['uri'];
$this->copyStreamToStream($value, $handle);
$this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY);
} else {
$this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR);
}
unset($value);
}
return $handles;
}
/**
* @return resource
*
* @throws Exception
*/
private function createTemporaryFile()
{
$handle = @tmpfile();
if ($handle === false) {
throw CannotCreateTemporaryFile::new(error_get_last());
}
return $handle;
}
/**
* @param resource $source
* @param resource $target
*
* @throws Exception
*/
private function copyStreamToStream($source, $target): void
{
if (@stream_copy_to_stream($source, $target) === false) {
throw CannotCopyStreamToStream::new(error_get_last());
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver;
interface Middleware
{
public function wrap(Driver $driver): Driver;
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement;
abstract class AbstractConnectionMiddleware implements Connection
{
public function __construct(private readonly Connection $wrappedConnection)
{
}
public function prepare(string $sql): Statement
{
return $this->wrappedConnection->prepare($sql);
}
public function query(string $sql): Result
{
return $this->wrappedConnection->query($sql);
}
public function quote(string $value): string
{
return $this->wrappedConnection->quote($value);
}
public function exec(string $sql): int|string
{
return $this->wrappedConnection->exec($sql);
}
public function lastInsertId(): int|string
{
return $this->wrappedConnection->lastInsertId();
}
public function beginTransaction(): void
{
$this->wrappedConnection->beginTransaction();
}
public function commit(): void
{
$this->wrappedConnection->commit();
}
public function rollBack(): void
{
$this->wrappedConnection->rollBack();
}
public function getServerVersion(): string
{
return $this->wrappedConnection->getServerVersion();
}
/**
* {@inheritDoc}
*/
public function getNativeConnection()
{
return $this->wrappedConnection->getNativeConnection();
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\ServerVersionProvider;
use SensitiveParameter;
abstract class AbstractDriverMiddleware implements Driver
{
public function __construct(private readonly Driver $wrappedDriver)
{
}
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): DriverConnection {
return $this->wrappedDriver->connect($params);
}
public function getDatabasePlatform(ServerVersionProvider $versionProvider): AbstractPlatform
{
return $this->wrappedDriver->getDatabasePlatform($versionProvider);
}
public function getExceptionConverter(): ExceptionConverter
{
return $this->wrappedDriver->getExceptionConverter();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Result;
use LogicException;
use function get_debug_type;
use function method_exists;
use function sprintf;
abstract class AbstractResultMiddleware implements Result
{
public function __construct(private readonly Result $wrappedResult)
{
}
public function fetchNumeric(): array|false
{
return $this->wrappedResult->fetchNumeric();
}
public function fetchAssociative(): array|false
{
return $this->wrappedResult->fetchAssociative();
}
public function fetchOne(): mixed
{
return $this->wrappedResult->fetchOne();
}
/**
* {@inheritDoc}
*/
public function fetchAllNumeric(): array
{
return $this->wrappedResult->fetchAllNumeric();
}
/**
* {@inheritDoc}
*/
public function fetchAllAssociative(): array
{
return $this->wrappedResult->fetchAllAssociative();
}
/**
* {@inheritDoc}
*/
public function fetchFirstColumn(): array
{
return $this->wrappedResult->fetchFirstColumn();
}
public function rowCount(): int|string
{
return $this->wrappedResult->rowCount();
}
public function columnCount(): int
{
return $this->wrappedResult->columnCount();
}
public function getColumnName(int $index): string
{
if (! method_exists($this->wrappedResult, 'getColumnName')) {
throw new LogicException(sprintf(
'The driver result %s does not support accessing the column name.',
get_debug_type($this->wrappedResult),
));
}
return $this->wrappedResult->getColumnName($index);
}
public function free(): void
{
$this->wrappedResult->free();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\ParameterType;
abstract class AbstractStatementMiddleware implements Statement
{
public function __construct(private readonly Statement $wrappedStatement)
{
}
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
{
$this->wrappedStatement->bindValue($param, $value, $type);
}
public function execute(): Result
{
return $this->wrappedStatement->execute();
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError;
use mysqli;
use mysqli_sql_exception;
final class Connection implements ConnectionInterface
{
/**
* Name of the option to set connection flags
*/
public const OPTION_FLAGS = 'flags';
/** @internal The connection can be only instantiated by its driver. */
public function __construct(private readonly mysqli $connection)
{
}
public function getServerVersion(): string
{
return $this->connection->get_server_info();
}
public function prepare(string $sql): Statement
{
try {
$stmt = $this->connection->prepare($sql);
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
if ($stmt === false) {
throw ConnectionError::new($this->connection);
}
return new Statement($stmt);
}
public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}
public function quote(string $value): string
{
return "'" . $this->connection->escape_string($value) . "'";
}
public function exec(string $sql): int|string
{
try {
$result = $this->connection->query($sql);
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
if ($result === false) {
throw ConnectionError::new($this->connection);
}
return $this->connection->affected_rows;
}
public function lastInsertId(): int|string
{
$lastInsertId = $this->connection->insert_id;
if ($lastInsertId === 0) {
throw Exception\NoIdentityValue::new();
}
return $this->connection->insert_id;
}
public function beginTransaction(): void
{
if (! $this->connection->begin_transaction()) {
throw ConnectionError::new($this->connection);
}
}
public function commit(): void
{
try {
if (! $this->connection->commit()) {
throw ConnectionError::new($this->connection);
}
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
}
public function rollBack(): void
{
try {
if (! $this->connection->rollback()) {
throw ConnectionError::new($this->connection);
}
} catch (mysqli_sql_exception $e) {
throw ConnectionError::upcast($e);
}
}
public function getNativeConnection(): mysqli
{
return $this->connection;
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli;
use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionFailed;
use Doctrine\DBAL\Driver\Mysqli\Exception\HostRequired;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Charset;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Options;
use Doctrine\DBAL\Driver\Mysqli\Initializer\Secure;
use Generator;
use mysqli;
use mysqli_sql_exception;
use SensitiveParameter;
final class Driver extends AbstractMySQLDriver
{
/**
* {@inheritDoc}
*/
public function connect(
#[SensitiveParameter]
array $params,
): Connection {
if (! empty($params['persistent'])) {
if (! isset($params['host'])) {
throw HostRequired::forPersistentConnection();
}
$host = 'p:' . $params['host'];
} else {
$host = $params['host'] ?? '';
}
$connection = new mysqli();
foreach ($this->compilePreInitializers($params) as $initializer) {
$initializer->initialize($connection);
}
try {
$success = @$connection->real_connect(
$host,
$params['user'] ?? '',
$params['password'] ?? '',
$params['dbname'] ?? '',
$params['port'] ?? 0,
$params['unix_socket'] ?? '',
$params['driverOptions'][Connection::OPTION_FLAGS] ?? 0,
);
} catch (mysqli_sql_exception $e) {
throw ConnectionFailed::upcast($e);
}
if (! $success) {
throw ConnectionFailed::new($connection);
}
foreach ($this->compilePostInitializers($params) as $initializer) {
$initializer->initialize($connection);
}
return new Connection($connection);
}
/**
* @param array<string, mixed> $params
*
* @return Generator<int, Initializer>
*/
private function compilePreInitializers(
#[SensitiveParameter]
array $params,
): Generator {
unset($params['driverOptions'][Connection::OPTION_FLAGS]);
if (isset($params['driverOptions']) && $params['driverOptions'] !== []) {
yield new Options($params['driverOptions']);
}
if (
! isset($params['ssl_key']) &&
! isset($params['ssl_cert']) &&
! isset($params['ssl_ca']) &&
! isset($params['ssl_capath']) &&
! isset($params['ssl_cipher'])
) {
return;
}
yield new Secure(
$params['ssl_key'] ?? '',
$params['ssl_cert'] ?? '',
$params['ssl_ca'] ?? '',
$params['ssl_capath'] ?? '',
$params['ssl_cipher'] ?? '',
);
}
/**
* @param array<string, mixed> $params
*
* @return Generator<int, Initializer>
*/
private function compilePostInitializers(
#[SensitiveParameter]
array $params,
): Generator {
if (! isset($params['charset'])) {
return;
}
yield new Charset($params['charset']);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
/** @internal */
final class ConnectionError extends AbstractException
{
public static function new(mysqli $connection): self
{
return new self($connection->error, $connection->sqlstate, $connection->errno);
}
public static function upcast(mysqli_sql_exception $exception): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
use function assert;
/** @internal */
final class ConnectionFailed extends AbstractException
{
public static function new(mysqli $connection): self
{
$error = $connection->connect_error;
assert($error !== null);
return new self($error, 'HY000', $connection->connect_errno);
}
public static function upcast(mysqli_sql_exception $exception): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self($exception->getMessage(), $p->getValue($exception), $exception->getCode(), $exception);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class FailedReadingStreamOffset extends AbstractException
{
public static function new(int $parameter): self
{
return new self(sprintf('Failed reading the stream resource for parameter #%d.', $parameter));
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
/** @internal */
final class HostRequired extends AbstractException
{
public static function forPersistentConnection(): self
{
return new self('The "host" parameter is required for a persistent connection');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use mysqli;
use mysqli_sql_exception;
use ReflectionProperty;
use function sprintf;
/** @internal */
final class InvalidCharset extends AbstractException
{
public static function fromCharset(mysqli $connection, string $charset): self
{
return new self(
sprintf('Failed to set charset "%s": %s', $charset, $connection->error),
$connection->sqlstate,
$connection->errno,
);
}
public static function upcast(mysqli_sql_exception $exception, string $charset): self
{
$p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate');
return new self(
sprintf('Failed to set charset "%s": %s', $charset, $exception->getMessage()),
$p->getValue($exception),
$exception->getCode(),
$exception,
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\DBAL\Driver\Mysqli\Exception;
use Doctrine\DBAL\Driver\AbstractException;
use function sprintf;
/** @internal */
final class InvalidOption extends AbstractException
{
public static function fromOption(int $option, mixed $value): self
{
return new self(
sprintf('Failed to set option %d with value "%s"', $option, $value),
);
}
}

Some files were not shown because too many files have changed in this diff Show More